Merge branch 'develop' into feature/fga/room_list_filters

This commit is contained in:
ganfra 2024-02-16 11:30:20 +01:00
commit 9b2198f12b
1489 changed files with 5726 additions and 4385 deletions

View file

@ -56,7 +56,7 @@ jobs:
path: | path: |
app/build/outputs/apk/gplay/debug/*.apk app/build/outputs/apk/gplay/debug/*.apk
app/build/outputs/apk/fdroid/debug/*.apk app/build/outputs/apk/fdroid/debug/*.apk
- uses: rnkdsh/action-upload-diawi@v1.5.4 - uses: rnkdsh/action-upload-diawi@v1.5.5
id: diawi id: diawi
# Do not fail the whole build if Diawi upload fails # Do not fail the whole build if Diawi upload fails
continue-on-error: true continue-on-error: true

View file

@ -8,6 +8,7 @@ appId: ${APP_ID}
# Back from timeline # Back from timeline
- back - back
- assertVisible: "MyR" - assertVisible: "MyR"
- hideKeyboard
# Back from search # Back from search
- back - back
- runFlow: ../assertions/assertHomeDisplayed.yaml - runFlow: ../assertions/assertHomeDisplayed.yaml

View file

@ -1,3 +1,42 @@
Changes in Element X v0.4.4 (2024-02-15)
========================================
Bugfixes 🐛
----------
- Fix decryption of previous messages after session verification not working.
Changes in Element X v0.4.3 (2024-02-14)
========================================
Features ✨
----------
- Change "Read receipts" advanced setting used to send private Read Receipt to "Share presence" settings. When disabled, private Read Receipts will be sent, and no typing notification will be sent. Also Read Receipts and typing notifications will not be rendered in the timeline. ([#2241](https://github.com/element-hq/element-x-android/issues/2241))
- Render typing notifications. ([#2242](https://github.com/element-hq/element-x-android/issues/2242))
- Manually mark a room as unread. ([#2261](https://github.com/element-hq/element-x-android/issues/2261))
- Add empty state to the room list. ([#2330](https://github.com/element-hq/element-x-android/issues/2330))
- Allow joining unencrypted video calls in non encrypted rooms. ([#2333](https://github.com/element-hq/element-x-android/issues/2333))
Bugfixes 🐛
----------
- Fix crash after unregistering UnifiedPush distributor ([#2304](https://github.com/element-hq/element-x-android/issues/2304))
- Add missing device id to settings screen. ([#2316](https://github.com/element-hq/element-x-android/issues/2316))
- Open the keyboard (and keep it opened) when creating a poll. ([#2329](https://github.com/element-hq/element-x-android/issues/2329))
- Fix message forwarding after SDK API change related to Timeline intitialization.
Other changes
-------------
- Adjusted the login flow buttons so the continue button is always at the same height ([#825](https://github.com/element-hq/element-x-android/issues/825))
- Move migration screen to within the room list ([#2310](https://github.com/element-hq/element-x-android/issues/2310))
- Render correctly in reply to data when Event cannot be decrypted or has been redacted ([#2318](https://github.com/element-hq/element-x-android/issues/2318))
- Remove Compose Foundation version pinning workaround. This was done to avoid a bug introduced in the default foundation version used by the material3 library, but that has already been fixed.
- Remove `FilterHiddenStateEventsProcessor`, as this is already handled by the Rust SDK.
- Remove session preferences on user log out.
Breaking changes 🚨
-------------------
- Update Compound icons in the project. Since the icon prefix changed to `ic_compound_` and the `CompoundIcons` helper now contains the vector icons as composable functions.
Changes in Element X v0.4.2 (2024-01-31) Changes in Element X v0.4.2 (2024-01-31)
======================================== ========================================

View file

@ -35,10 +35,8 @@ import io.element.android.x.R
@Preview @Preview
@Composable @Composable
internal fun IconPreview( internal fun IconPreview() {
modifier: Modifier = Modifier, Box {
) {
Box(modifier = modifier) {
Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null) Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null)
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null) Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null)
} }
@ -46,10 +44,8 @@ internal fun IconPreview(
@Preview @Preview
@Composable @Composable
internal fun RoundIconPreview( internal fun RoundIconPreview() {
modifier: Modifier = Modifier, Box(modifier = Modifier.clip(shape = CircleShape)) {
) {
Box(modifier = modifier.clip(shape = CircleShape)) {
Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null) Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null)
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null) Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null)
} }
@ -57,11 +53,9 @@ internal fun RoundIconPreview(
@Preview @Preview
@Composable @Composable
internal fun MonochromeIconPreview( internal fun MonochromeIconPreview() {
modifier: Modifier = Modifier,
) {
Box( Box(
modifier = modifier modifier = Modifier
.size(108.dp) .size(108.dp)
.background(Color(0xFF2F3133)) .background(Color(0xFF2F3133))
.clip(shape = RoundedCornerShape(32.dp)), .clip(shape = RoundedCornerShape(32.dp)),

View file

@ -84,10 +84,8 @@ fun LoadingRoomNodeView(
@Composable @Composable
private fun LoadingRoomTopBar( private fun LoadingRoomTopBar(
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
modifier: Modifier = Modifier
) { ) {
TopAppBar( TopAppBar(
modifier = modifier,
navigationIcon = { navigationIcon = {
BackButton(onClick = onBackClicked) BackButton(onClick = onBackClicked)
}, },

View file

@ -28,6 +28,7 @@ import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.rageshake.FakeRageShake import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.impl.DefaultAppErrorStateService import io.element.android.services.apperror.impl.DefaultAppErrorStateService
@ -83,6 +84,7 @@ class RootPresenterTest {
val rageshake = FakeRageShake() val rageshake = FakeRageShake()
val screenshotHolder = FakeScreenshotHolder() val screenshotHolder = FakeScreenshotHolder()
val crashDetectionPresenter = DefaultCrashDetectionPresenter( val crashDetectionPresenter = DefaultCrashDetectionPresenter(
buildMeta = aBuildMeta(),
crashDataStore = crashDataStore crashDataStore = crashDataStore
) )
val rageshakeDetectionPresenter = DefaultRageshakeDetectionPresenter( val rageshakeDetectionPresenter = DefaultRageshakeDetectionPresenter(

View file

@ -1 +0,0 @@
Fix message forwarding after SDK API change related to Timeline intitialization.

View file

@ -1 +0,0 @@
Remove Compose Foundation version pinning workaround. This was done to avoid a bug introduced in the default foundation version used by the material3 library, but that has already been fixed.

View file

@ -1 +0,0 @@
Remove session preferences on user log out.

1
changelog.d/2208.feature Normal file
View file

@ -0,0 +1 @@
Mark a room or dm as favourite.

View file

@ -1,2 +0,0 @@
Change "Read receipts" advanced setting used to send private Read Receipt to "Share presence" settings.
When disabled, private Read Receipts will be sent, and no typing notification will be sent. Also Read Receipts and typing notifications will not be rendered in the timeline.

View file

@ -1 +0,0 @@
Fix crash after unregistering UnifiedPush distributor

View file

@ -1 +0,0 @@
Add missing device id to settings screen.

View file

@ -1 +0,0 @@
Add empty state to the room list.

View file

@ -1 +0,0 @@
Allow joining unencrypted video calls in non encrypted rooms.

View file

@ -1 +0,0 @@
Adjusted the login flow buttons so the continue button is always at the same height

View file

@ -0,0 +1,10 @@
Main changes in this version:
- Added share presence toggle.
- Render typing notifications.
- Manually mark a room as unread.
- Add an empty state to the room list.
- Allow joining unencrypted video calls in non encrypted rooms.
And several other bugfixes.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,4 @@
Main changes in this version:
- Fix decryption of previous messages after session verification not working.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -57,7 +57,6 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -67,7 +66,6 @@ fun AnalyticsOptInView(
onClickTerms: () -> Unit, onClickTerms: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LogCompositions(tag = "Analytics", msg = "Root")
val eventSink = state.eventSink val eventSink = state.eventSink
fun onTermsAccepted() { fun onTermsAccepted() {
@ -101,10 +99,8 @@ private const val LINK_TAG = "link"
private fun AnalyticsOptInHeader( private fun AnalyticsOptInHeader(
state: AnalyticsOptInState, state: AnalyticsOptInState,
onClickTerms: () -> Unit, onClickTerms: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
@ -141,24 +137,22 @@ private fun AnalyticsOptInHeader(
} }
@Composable @Composable
private fun CheckIcon(modifier: Modifier = Modifier) { private fun CheckIcon() {
Icon( Icon(
modifier = modifier modifier = Modifier
.size(20.dp) .size(20.dp)
.background(color = MaterialTheme.colorScheme.background, shape = CircleShape) .background(color = MaterialTheme.colorScheme.background, shape = CircleShape)
.padding(2.dp), .padding(2.dp),
imageVector = CompoundIcons.Check, imageVector = CompoundIcons.Check(),
contentDescription = null, contentDescription = null,
tint = ElementTheme.colors.textActionAccent, tint = ElementTheme.colors.textActionAccent,
) )
} }
@Composable @Composable
private fun AnalyticsOptInContent( private fun AnalyticsOptInContent() {
modifier: Modifier = Modifier,
) {
Box( Box(
modifier = modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = BiasAlignment( contentAlignment = BiasAlignment(
horizontalBias = 0f, horizontalBias = 0f,
verticalBias = -0.4f verticalBias = -0.4f
@ -190,11 +184,8 @@ private fun AnalyticsOptInContent(
private fun AnalyticsOptInFooter( private fun AnalyticsOptInFooter(
onTermsAccepted: () -> Unit, onTermsAccepted: () -> Unit,
onTermsDeclined: () -> Unit, onTermsDeclined: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
ButtonColumnMolecule( ButtonColumnMolecule {
modifier = modifier,
) {
Button( Button(
text = stringResource(id = CommonStrings.action_ok), text = stringResource(id = CommonStrings.action_ok),
onClick = onTermsAccepted, onClick = onTermsAccepted,

View file

@ -64,7 +64,7 @@ internal fun CallScreenView(
title = { Text(stringResource(R.string.element_call)) }, title = { Text(stringResource(R.string.element_call)) },
navigationIcon = { navigationIcon = {
BackButton( BackButton(
imageVector = CompoundIcons.Close, imageVector = CompoundIcons.Close(),
onClick = { state.eventSink(CallScreenEvents.Hangup) } onClick = { state.eventSink(CallScreenEvents.Hangup) }
) )
} }

View file

@ -88,10 +88,8 @@ private fun AddPeopleViewTopBar(
hasSelectedUsers: Boolean, hasSelectedUsers: Boolean,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
onNextPressed: () -> Unit, onNextPressed: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
TopAppBar( TopAppBar(
modifier = modifier,
title = { title = {
Text( Text(
text = stringResource(id = R.string.screen_create_room_add_people_title), text = stringResource(id = R.string.screen_create_room_add_people_title),

View file

@ -18,7 +18,6 @@ package io.element.android.features.createroom.impl.configureroom
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@ -38,8 +37,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
@ -52,6 +49,7 @@ import io.element.android.libraries.designsystem.components.LabelledTextField
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.aliasScreenTitle
@ -173,10 +171,8 @@ private fun ConfigureRoomToolbar(
isNextActionEnabled: Boolean, isNextActionEnabled: Boolean,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
onNextPressed: () -> Unit, onNextPressed: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
TopAppBar( TopAppBar(
modifier = modifier,
title = { title = {
Text( Text(
text = stringResource(R.string.screen_create_room_title), text = stringResource(R.string.screen_create_room_title),
@ -259,13 +255,6 @@ private fun RoomPrivacyOptions(
} }
} }
private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
pointerInput(Unit) {
detectTapGestures(onTap = {
focusManager.clearFocus()
})
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview { internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {

View file

@ -33,18 +33,18 @@ data class RoomPrivacyItem(
@Composable @Composable
fun roomPrivacyItems(): ImmutableList<RoomPrivacyItem> { fun roomPrivacyItems(): ImmutableList<RoomPrivacyItem> {
return RoomPrivacy.values() return RoomPrivacy.entries
.map { .map {
when (it) { when (it) {
RoomPrivacy.Private -> RoomPrivacyItem( RoomPrivacy.Private -> RoomPrivacyItem(
privacy = it, privacy = it,
icon = CompoundDrawables.ic_lock, icon = CompoundDrawables.ic_compound_lock_solid,
title = stringResource(R.string.screen_create_room_private_option_title), title = stringResource(R.string.screen_create_room_private_option_title),
description = stringResource(R.string.screen_create_room_private_option_description), description = stringResource(R.string.screen_create_room_private_option_description),
) )
RoomPrivacy.Public -> RoomPrivacyItem( RoomPrivacy.Public -> RoomPrivacyItem(
privacy = it, privacy = it,
icon = CompoundDrawables.ic_public, icon = CompoundDrawables.ic_compound_public,
title = stringResource(R.string.screen_create_room_public_option_title), title = stringResource(R.string.screen_create_room_public_option_title),
description = stringResource(R.string.screen_create_room_public_option_description), description = stringResource(R.string.screen_create_room_public_option_description),
) )

View file

@ -117,10 +117,8 @@ fun CreateRoomRootView(
@Composable @Composable
private fun CreateRoomRootViewTopBar( private fun CreateRoomRootViewTopBar(
onClosePressed: () -> Unit, onClosePressed: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
TopAppBar( TopAppBar(
modifier = modifier,
title = { title = {
Text( Text(
text = stringResource(id = CommonStrings.action_start_chat), text = stringResource(id = CommonStrings.action_start_chat),
@ -129,7 +127,7 @@ private fun CreateRoomRootViewTopBar(
}, },
navigationIcon = { navigationIcon = {
BackButton( BackButton(
imageVector = CompoundIcons.Close, imageVector = CompoundIcons.Close(),
onClick = onClosePressed, onClick = onClosePressed,
) )
} }
@ -141,16 +139,15 @@ private fun CreateRoomActionButtonsList(
state: CreateRoomRootState, state: CreateRoomRootState,
onNewRoomClicked: () -> Unit, onNewRoomClicked: () -> Unit,
onInvitePeopleClicked: () -> Unit, onInvitePeopleClicked: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier) { Column {
CreateRoomActionButton( CreateRoomActionButton(
iconRes = CompoundDrawables.ic_plus, iconRes = CompoundDrawables.ic_compound_plus,
text = stringResource(id = R.string.screen_create_room_action_create_room), text = stringResource(id = R.string.screen_create_room_action_create_room),
onClick = onNewRoomClicked, onClick = onNewRoomClicked,
) )
CreateRoomActionButton( CreateRoomActionButton(
iconRes = CompoundDrawables.ic_share_android, iconRes = CompoundDrawables.ic_compound_share_android,
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName), text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
onClick = onInvitePeopleClicked, onClick = onInvitePeopleClicked,
) )
@ -162,10 +159,9 @@ private fun CreateRoomActionButton(
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp) .height(56.dp)
.clickable { onClick() } .clickable { onClick() }

View file

@ -33,7 +33,6 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.impl.migration.MigrationScreenNode
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.DefaultFtueState
import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.state.FtueStep
@ -74,9 +73,6 @@ class FtueFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data object Placeholder : NavTarget data object Placeholder : NavTarget
@Parcelize
data object MigrationScreen : NavTarget
@Parcelize @Parcelize
data object WelcomeScreen : NavTarget data object WelcomeScreen : NavTarget
@ -114,14 +110,6 @@ class FtueFlowNode @AssistedInject constructor(
NavTarget.Placeholder -> { NavTarget.Placeholder -> {
createNode<PlaceholderNode>(buildContext) createNode<PlaceholderNode>(buildContext)
} }
NavTarget.MigrationScreen -> {
val callback = object : MigrationScreenNode.Callback {
override fun onMigrationFinished() {
lifecycleScope.launch { moveToNextStep() }
}
}
createNode<MigrationScreenNode>(buildContext, listOf(callback))
}
NavTarget.WelcomeScreen -> { NavTarget.WelcomeScreen -> {
val callback = object : WelcomeNode.Callback { val callback = object : WelcomeNode.Callback {
override fun onContinueClicked() { override fun onContinueClicked() {
@ -158,9 +146,6 @@ class FtueFlowNode @AssistedInject constructor(
private fun moveToNextStep() { private fun moveToNextStep() {
when (ftueState.getNextStep()) { when (ftueState.getNextStep()) {
FtueStep.MigrationScreen -> {
backstack.newRoot(NavTarget.MigrationScreen)
}
FtueStep.WelcomeScreen -> { FtueStep.WelcomeScreen -> {
backstack.newRoot(NavTarget.WelcomeScreen) backstack.newRoot(NavTarget.WelcomeScreen)
} }

View file

@ -1,52 +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.ftue.impl.migration
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class MigrationScreenNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: MigrationScreenPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onMigrationFinished()
}
private fun onMigrationFinished() {
plugins.filterIsInstance<Callback>().forEach { it.onMigrationFinished() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
MigrationScreenView(
state,
onMigrationFinished = ::onMigrationFinished,
modifier = modifier
)
}
}

View file

@ -67,7 +67,7 @@ fun NotificationsOptInView(
header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) },
footer = { NotificationsOptInFooter(state) }, footer = { NotificationsOptInFooter(state) },
) { ) {
NotificationsOptInContent(modifier = Modifier.fillMaxWidth()) NotificationsOptInContent()
} }
} }
@ -79,7 +79,7 @@ private fun NotificationsOptInHeader(
modifier = modifier, modifier = modifier,
title = stringResource(R.string.screen_notification_optin_title), title = stringResource(R.string.screen_notification_optin_title),
subTitle = stringResource(R.string.screen_notification_optin_subtitle), subTitle = stringResource(R.string.screen_notification_optin_subtitle),
iconImageVector = CompoundIcons.NotificationsSolid, iconImageVector = CompoundIcons.NotificationsSolid(),
) )
} }
@ -104,10 +104,8 @@ private fun NotificationsOptInFooter(state: NotificationsOptInState) {
} }
@Composable @Composable
private fun NotificationsOptInContent( private fun NotificationsOptInContent() {
modifier: Modifier = Modifier, Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column( Column(
verticalArrangement = Arrangement.spacedBy( verticalArrangement = Arrangement.spacedBy(
16.dp, 16.dp,
@ -144,10 +142,8 @@ private fun NotificationRow(
avatarColorsId: String, avatarColorsId: String,
firstRowPercent: Float, firstRowPercent: Float,
secondRowPercent: Float, secondRowPercent: Float,
modifier: Modifier = Modifier
) { ) {
Surface( Surface(
modifier = modifier,
color = ElementTheme.colors.bgCanvasDisabled, color = ElementTheme.colors.bgCanvasDisabled,
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
shadowElevation = 2.dp, shadowElevation = 2.dp,

View file

@ -21,11 +21,9 @@ import android.os.Build
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ -43,17 +41,14 @@ class DefaultFtueState @Inject constructor(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val welcomeScreenState: WelcomeScreenState, private val welcomeScreenState: WelcomeScreenState,
private val migrationScreenStore: MigrationScreenStore,
private val permissionStateProvider: PermissionStateProvider, private val permissionStateProvider: PermissionStateProvider,
private val lockScreenService: LockScreenService, private val lockScreenService: LockScreenService,
private val matrixClient: MatrixClient,
) : FtueState { ) : FtueState {
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete()) override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
override suspend fun reset() { override suspend fun reset() {
welcomeScreenState.reset() welcomeScreenState.reset()
analyticsService.reset() analyticsService.reset()
migrationScreenStore.reset()
if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS) permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS)
} }
@ -67,12 +62,7 @@ class DefaultFtueState @Inject constructor(
fun getNextStep(currentStep: FtueStep? = null): FtueStep? = fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
when (currentStep) { when (currentStep) {
null -> if (shouldDisplayMigrationScreen()) { null -> if (shouldDisplayWelcomeScreen()) {
FtueStep.MigrationScreen
} else {
getNextStep(FtueStep.MigrationScreen)
}
FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) {
FtueStep.WelcomeScreen FtueStep.WelcomeScreen
} else { } else {
getNextStep(FtueStep.WelcomeScreen) getNextStep(FtueStep.WelcomeScreen)
@ -97,7 +87,6 @@ class DefaultFtueState @Inject constructor(
private fun isAnyStepIncomplete(): Boolean { private fun isAnyStepIncomplete(): Boolean {
return listOf( return listOf(
{ shouldDisplayMigrationScreen() },
{ shouldDisplayWelcomeScreen() }, { shouldDisplayWelcomeScreen() },
{ shouldAskNotificationPermissions() }, { shouldAskNotificationPermissions() },
{ needsAnalyticsOptIn() }, { needsAnalyticsOptIn() },
@ -105,10 +94,6 @@ class DefaultFtueState @Inject constructor(
).any { it() } ).any { it() }
} }
private fun shouldDisplayMigrationScreen(): Boolean {
return migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)
}
private fun needsAnalyticsOptIn(): Boolean { private fun needsAnalyticsOptIn(): Boolean {
// We need this function to not be suspend, so we need to load the value through runBlocking // We need this function to not be suspend, so we need to load the value through runBlocking
return runBlocking { analyticsService.didAskUserConsent().first().not() } return runBlocking { analyticsService.didAskUserConsent().first().not() }
@ -147,7 +132,6 @@ class DefaultFtueState @Inject constructor(
} }
sealed interface FtueStep { sealed interface FtueStep {
data object MigrationScreen : FtueStep
data object WelcomeScreen : FtueStep data object WelcomeScreen : FtueStep
data object NotificationsOptIn : FtueStep data object NotificationsOptIn : FtueStep
data object AnalyticsOptIn : FtueStep data object AnalyticsOptIn : FtueStep

View file

@ -43,7 +43,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -102,11 +101,11 @@ fun WelcomeView(
private fun listItems() = persistentListOf( private fun listItems() = persistentListOf(
InfoListItem( InfoListItem(
message = stringResource(R.string.screen_welcome_bullet_2), message = stringResource(R.string.screen_welcome_bullet_2),
iconId = CommonDrawables.ic_lock_outline, iconVector = CompoundIcons.Lock(),
), ),
InfoListItem( InfoListItem(
message = stringResource(R.string.screen_welcome_bullet_3), message = stringResource(R.string.screen_welcome_bullet_3),
iconVector = CompoundIcons.ChatProblem, iconVector = CompoundIcons.ChatProblem(),
), ),
) )

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Гэта аднаразовы працэс, дзякуем за чаканне."</string>
<string name="screen_migration_title">"Налада ўліковага запісу."</string>
<string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string> <string name="screen_notification_optin_subtitle">"Вы можаце змяніць налады пазней."</string>
<string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string> <string name="screen_notification_optin_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
<string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string> <string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Jedná se o jednorázový proces, prosíme o strpení."</string>
<string name="screen_migration_title">"Nastavení vašeho účtu"</string>
<string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string> <string name="screen_notification_optin_subtitle">"Nastavení můžete později změnit."</string>
<string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string> <string name="screen_notification_optin_title">"Povolte oznámení a nezmeškejte žádnou zprávu"</string>
<string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string> <string name="screen_welcome_bullet_1">"Hovory, hlasování, vyhledávání a další budou přidány koncem tohoto roku."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Dies ist ein einmaliger Vorgang, danke fürs Warten."</string>
<string name="screen_migration_title">"Dein Konto wird eingerichtet."</string>
<string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string> <string name="screen_notification_optin_subtitle">"Du kannst deine Einstellungen später ändern."</string>
<string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string> <string name="screen_notification_optin_title">"Erlaube Benachrichtigungen und verpasse keine Nachricht"</string>
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string> <string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Este proceso solo se hace una vez, gracias por esperar."</string>
<string name="screen_migration_title">"Configura tu cuenta"</string>
<string name="screen_notification_optin_subtitle">"Puedes cambiar la configuración más tarde."</string> <string name="screen_notification_optin_subtitle">"Puedes cambiar la configuración más tarde."</string>
<string name="screen_notification_optin_title">"Activa las notificaciones y nunca te pierdas un mensaje"</string> <string name="screen_notification_optin_title">"Activa las notificaciones y nunca te pierdas un mensaje"</string>
<string name="screen_welcome_bullet_1">"Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año."</string> <string name="screen_welcome_bullet_1">"Las llamadas, las encuestas, la búsqueda y más se agregarán más adelante este año."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Il sagit dune opération ponctuelle, merci dattendre quelques instants."</string>
<string name="screen_migration_title">"Configuration de votre compte."</string>
<string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string> <string name="screen_notification_optin_subtitle">"Vous pourrez modifier vos paramètres ultérieurement."</string>
<string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string> <string name="screen_notification_optin_title">"Autorisez les notifications et ne manquez aucun message"</string>
<string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string> <string name="screen_welcome_bullet_1">"Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Ez egy egyszeri folyamat, köszönjük a türelmét."</string>
<string name="screen_migration_title">"A fiók beállítása."</string>
<string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string> <string name="screen_notification_optin_subtitle">"A beállításokat később is módosíthatja."</string>
<string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string> <string name="screen_notification_optin_title">"Értesítések engedélyezése, hogy soha ne maradjon le egyetlen üzenetről sem"</string>
<string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string> <string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Ini adalah proses satu kali, terima kasih telah menunggu."</string>
<string name="screen_migration_title">"Menyiapkan akun Anda."</string>
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string> <string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string> <string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
<string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string> <string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Si tratta di una procedura che si effettua una sola volta, grazie per l\'attesa."</string>
<string name="screen_migration_title">"Configurazione del tuo account."</string>
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string> <string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string> <string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string> <string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Acesta este un proces care se desfășoară o singură dată, vă mulțumim pentru așteptare."</string>
<string name="screen_migration_title">"Contul dumneavoastră se configurează"</string>
<string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string> <string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
<string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string> <string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string>
<string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string> <string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Это одноразовый процесс, спасибо, что подождали."</string>
<string name="screen_migration_title">"Настройка учетной записи."</string>
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string> <string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string> <string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string> <string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"Ide o jednorazový proces, ďakujeme za trpezlivosť."</string>
<string name="screen_migration_title">"Nastavenie vášho účtu."</string>
<string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string> <string name="screen_notification_optin_subtitle">"Svoje nastavenia môžete neskôr zmeniť."</string>
<string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string> <string name="screen_notification_optin_title">"Povoľte oznámenia a nikdy nezmeškajte žiadnu správu"</string>
<string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string> <string name="screen_welcome_bullet_1">"Hovory, ankety, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"這是一次性的程序,感謝您耐心等候。"</string>
<string name="screen_migration_title">"正在設定您的帳號。"</string>
<string name="screen_welcome_bullet_1">"通話、投票、搜尋等更多功能將在今年登場。"</string> <string name="screen_welcome_bullet_1">"通話、投票、搜尋等更多功能將在今年登場。"</string>
<string name="screen_welcome_bullet_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</string> <string name="screen_welcome_bullet_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</string>
<string name="screen_welcome_bullet_3">"我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"</string> <string name="screen_welcome_bullet_3">"我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
<string name="screen_migration_title">"Setting up your account."</string>
<string name="screen_notification_optin_subtitle">"You can change your settings later."</string> <string name="screen_notification_optin_subtitle">"You can change your settings later."</string>
<string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string> <string name="screen_notification_optin_title">"Allow notifications and never miss a message"</string>
<string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string> <string name="screen_welcome_bullet_1">"Calls, polls, search and more will be added later this year."</string>

View file

@ -18,16 +18,11 @@ package io.element.android.features.ftue.impl
import android.os.Build import android.os.Build
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.ftue.impl.migration.InMemoryMigrationScreenStore
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.DefaultFtueState
import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.state.FtueStep
import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.test.FakeLockScreenService import io.element.android.features.lockscreen.test.FakeLockScreenService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
@ -54,7 +49,6 @@ class DefaultFtueStateTests {
fun `given all checks being true, should display flow is false`() = runTest { fun `given all checks being true, should display flow is false`() = runTest {
val welcomeState = FakeWelcomeState() val welcomeState = FakeWelcomeState()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val migrationScreenStore = InMemoryMigrationScreenStore()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
@ -63,14 +57,12 @@ class DefaultFtueStateTests {
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
welcomeState = welcomeState, welcomeState = welcomeState,
analyticsService = analyticsService, analyticsService = analyticsService,
migrationScreenStore = migrationScreenStore,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
welcomeState.setWelcomeScreenShown() welcomeState.setWelcomeScreenShown()
analyticsService.setDidAskUserConsent() analyticsService.setDidAskUserConsent()
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
state.updateState() state.updateState()
@ -85,7 +77,6 @@ class DefaultFtueStateTests {
fun `traverse flow`() = runTest { fun `traverse flow`() = runTest {
val welcomeState = FakeWelcomeState() val welcomeState = FakeWelcomeState()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val migrationScreenStore = InMemoryMigrationScreenStore()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
@ -94,29 +85,24 @@ class DefaultFtueStateTests {
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
welcomeState = welcomeState, welcomeState = welcomeState,
analyticsService = analyticsService, analyticsService = analyticsService,
migrationScreenStore = migrationScreenStore,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
val steps = mutableListOf<FtueStep?>() val steps = mutableListOf<FtueStep?>()
// First step, migration screen // First step, welcome screen
steps.add(state.getNextStep(steps.lastOrNull()))
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
// Second step, welcome screen
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
welcomeState.setWelcomeScreenShown() welcomeState.setWelcomeScreenShown()
// Third step, notifications opt in // Second step, notifications opt in
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
// Fourth step, entering PIN code // Third step, entering PIN code
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
// Fifth step, analytics opt in // Fourth step, analytics opt in
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
analyticsService.setDidAskUserConsent() analyticsService.setDidAskUserConsent()
@ -124,7 +110,6 @@ class DefaultFtueStateTests {
steps.add(state.getNextStep(steps.lastOrNull())) steps.add(state.getNextStep(steps.lastOrNull()))
assertThat(steps).containsExactly( assertThat(steps).containsExactly(
FtueStep.MigrationScreen,
FtueStep.WelcomeScreen, FtueStep.WelcomeScreen,
FtueStep.NotificationsOptIn, FtueStep.NotificationsOptIn,
FtueStep.LockscreenSetup, FtueStep.LockscreenSetup,
@ -141,19 +126,16 @@ class DefaultFtueStateTests {
fun `if a check for a step is true, start from the next one`() = runTest { fun `if a check for a step is true, start from the next one`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val migrationScreenStore = InMemoryMigrationScreenStore()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val state = createState( val state = createState(
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
migrationScreenStore = migrationScreenStore,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
// Skip first 4 steps // Skip first 3 steps
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
state.setWelcomeScreenShown() state.setWelcomeScreenShown()
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
@ -171,18 +153,15 @@ class DefaultFtueStateTests {
fun `if version is older than 13 we don't display the notification opt in screen`() = runTest { fun `if version is older than 13 we don't display the notification opt in screen`() = runTest {
val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob())
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val migrationScreenStore = InMemoryMigrationScreenStore()
val lockScreenService = FakeLockScreenService() val lockScreenService = FakeLockScreenService()
val state = createState( val state = createState(
sdkIntVersion = Build.VERSION_CODES.M, sdkIntVersion = Build.VERSION_CODES.M,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
migrationScreenStore = migrationScreenStore,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
migrationScreenStore.setMigrationScreenShown(A_SESSION_ID)
assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen) assertThat(state.getNextStep()).isEqualTo(FtueStep.WelcomeScreen)
state.setWelcomeScreenShown() state.setWelcomeScreenShown()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
@ -200,9 +179,7 @@ class DefaultFtueStateTests {
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
welcomeState: FakeWelcomeState = FakeWelcomeState(), welcomeState: FakeWelcomeState = FakeWelcomeState(),
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(),
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
matrixClient: MatrixClient = FakeMatrixClient(),
lockScreenService: LockScreenService = FakeLockScreenService(), lockScreenService: LockScreenService = FakeLockScreenService(),
// First version where notification permission is required // First version where notification permission is required
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
@ -211,9 +188,7 @@ class DefaultFtueStateTests {
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
welcomeScreenState = welcomeState, welcomeScreenState = welcomeState,
migrationScreenStore = migrationScreenStore,
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
matrixClient = matrixClient,
) )
} }

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Bist du sicher, dass du diese Unterhaltung verlassen willst? Diese Unterhaltung ist nicht öffentlich und du kannst ihr ohne Einladung nicht wieder beitreten."</string>
<string name="leave_room_alert_empty_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du austritst, kann in Zukunft niemand mehr eintreten, auch du nicht."</string> <string name="leave_room_alert_empty_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du austritst, kann in Zukunft niemand mehr eintreten, auch du nicht."</string>
<string name="leave_room_alert_private_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten."</string> <string name="leave_room_alert_private_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten."</string>
<string name="leave_room_alert_subtitle">"Bist du sicher, dass du den Raum verlassen willst?"</string> <string name="leave_room_alert_subtitle">"Bist du sicher, dass du den Raum verlassen willst?"</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Biztos, hogy elhagyja ezt a beszélgetést? Ez a beszélgetés nem nyilvános, és meghívás nélkül nem fog tudni visszacsatlakozni."</string>
<string name="leave_room_alert_empty_subtitle">"Biztos, hogy elhagyja ezt a szobát? Ön az egyedüli ember itt. Ha kilép, akkor senki sem fog tudni csatlakozni a jövőben, Önt is beleértve."</string> <string name="leave_room_alert_empty_subtitle">"Biztos, hogy elhagyja ezt a szobát? Ön az egyedüli ember itt. Ha kilép, akkor senki sem fog tudni csatlakozni a jövőben, Önt is beleértve."</string>
<string name="leave_room_alert_private_subtitle">"Biztos, hogy elhagyod ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fogsz tudni újra belépni."</string> <string name="leave_room_alert_private_subtitle">"Biztos, hogy elhagyod ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fogsz tudni újra belépni."</string>
<string name="leave_room_alert_subtitle">"Biztos, hogy elhagyod a szobát?"</string> <string name="leave_room_alert_subtitle">"Biztos, hogy elhagyod a szobát?"</string>

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.camera.CameraPosition
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import io.element.android.features.location.api.internal.centerBottomEdge import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.features.location.api.internal.rememberTileStyleUrl import io.element.android.features.location.api.internal.rememberTileStyleUrl
@ -211,8 +212,8 @@ fun SendLocationView(
.padding(end = 16.dp, bottom = 72.dp + navBarPadding), .padding(end = 16.dp, bottom = 72.dp + navBarPadding),
) { ) {
when (state.mode) { when (state.mode) {
SendLocationState.Mode.PinLocation -> Icon(resourceId = CommonDrawables.ic_location_navigator, contentDescription = null) SendLocationState.Mode.PinLocation -> Icon(imageVector = CompoundIcons.LocationNavigator(), contentDescription = null)
SendLocationState.Mode.SenderLocation -> Icon(resourceId = CommonDrawables.ic_location_navigator_centered, contentDescription = null) SendLocationState.Mode.SenderLocation -> Icon(imageVector = CompoundIcons.LocationNavigatorCentred(), contentDescription = null)
} }
} }
} }

View file

@ -125,7 +125,7 @@ fun ShowLocationView(
actions = { actions = {
IconButton(onClick = { state.eventSink(ShowLocationEvents.Share) }) { IconButton(onClick = { state.eventSink(ShowLocationEvents.Share) }) {
Icon( Icon(
imageVector = CompoundIcons.ShareAndroid, imageVector = CompoundIcons.ShareAndroid(),
contentDescription = stringResource(CommonStrings.action_share), contentDescription = stringResource(CommonStrings.action_share),
) )
} }

View file

@ -66,10 +66,8 @@ fun PinEntryTextField(
private fun PinEntryRow( private fun PinEntryRow(
pinEntry: PinEntry, pinEntry: PinEntry,
isSecured: Boolean, isSecured: Boolean,
modifier: Modifier = Modifier,
) { ) {
FlowRow( FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
@ -83,7 +81,6 @@ private fun PinEntryRow(
private fun PinDigitView( private fun PinDigitView(
digit: PinDigit, digit: PinDigit,
isSecured: Boolean, isSecured: Boolean,
modifier: Modifier = Modifier,
) { ) {
val shape = RoundedCornerShape(8.dp) val shape = RoundedCornerShape(8.dp)
val appearanceModifier = when (digit) { val appearanceModifier = when (digit) {
@ -95,7 +92,7 @@ private fun PinDigitView(
} }
} }
Box( Box(
modifier = modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.then(appearanceModifier), .then(appearanceModifier),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,

View file

@ -57,13 +57,12 @@ fun SetupBiometricView(
} }
@Composable @Composable
private fun SetupBiometricHeader(modifier: Modifier = Modifier) { private fun SetupBiometricHeader() {
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication) val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
iconImageVector = Icons.Default.Fingerprint, iconImageVector = Icons.Default.Fingerprint,
title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock), title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock),
subTitle = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_subtitle, biometricAuth), subTitle = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_subtitle, biometricAuth),
modifier = modifier
) )
} }
@ -71,11 +70,8 @@ private fun SetupBiometricHeader(modifier: Modifier = Modifier) {
private fun SetupBiometricFooter( private fun SetupBiometricFooter(
onAllowClicked: () -> Unit, onAllowClicked: () -> Unit,
onSkipClicked: () -> Unit, onSkipClicked: () -> Unit,
modifier: Modifier = Modifier
) { ) {
ButtonColumnMolecule( ButtonColumnMolecule {
modifier = modifier,
) {
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication) val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
Button( Button(
text = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_allow_title, biometricAuth), text = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_allow_title, biometricAuth),

View file

@ -86,10 +86,8 @@ fun SetupPinView(
private fun SetupPinHeader( private fun SetupPinHeader(
isValidationStep: Boolean, isValidationStep: Boolean,
appName: String, appName: String,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
@ -107,7 +105,6 @@ private fun SetupPinHeader(
@Composable @Composable
private fun SetupPinContent( private fun SetupPinContent(
state: SetupPinState, state: SetupPinState,
modifier: Modifier = Modifier,
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -119,14 +116,13 @@ private fun SetupPinContent(
onValueChange = { entry -> onValueChange = { entry ->
state.eventSink(SetupPinEvents.OnPinEntryChanged(entry, state.isConfirmationStep)) state.eventSink(SetupPinEvents.OnPinEntryChanged(entry, state.isConfirmationStep))
}, },
modifier = modifier modifier = Modifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
.padding(top = 36.dp) .padding(top = 36.dp)
.fillMaxWidth() .fillMaxWidth()
) )
if (state.setupPinFailure != null) { if (state.setupPinFailure != null) {
ErrorDialog( ErrorDialog(
modifier = modifier,
title = state.setupPinFailure.title(), title = state.setupPinFailure.title(),
content = state.setupPinFailure.content(), content = state.setupPinFailure.content(),
onDismiss = { onDismiss = {

View file

@ -107,10 +107,9 @@ fun PinUnlockView(
private fun PinUnlockPage( private fun PinUnlockPage(
state: PinUnlockState, state: PinUnlockState,
isInAppUnlock: Boolean, isInAppUnlock: Boolean,
modifier: Modifier = Modifier
) { ) {
BoxWithConstraints { BoxWithConstraints {
val commonModifier = modifier val commonModifier = Modifier
.fillMaxSize() .fillMaxSize()
.systemBarsPadding() .systemBarsPadding()
.imePadding() .imePadding()
@ -188,7 +187,6 @@ private fun SignOutPrompt(
isCancellable: Boolean, isCancellable: Boolean,
onSignOut: () -> Unit, onSignOut: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier
) { ) {
if (isCancellable) { if (isCancellable) {
ConfirmationDialog( ConfirmationDialog(
@ -196,14 +194,12 @@ private fun SignOutPrompt(
content = stringResource(id = R.string.screen_app_lock_signout_alert_message), content = stringResource(id = R.string.screen_app_lock_signout_alert_message),
onSubmitClicked = onSignOut, onSubmitClicked = onSignOut,
onDismiss = onDismiss, onDismiss = onDismiss,
modifier = modifier,
) )
} else { } else {
ErrorDialog( ErrorDialog(
title = stringResource(id = R.string.screen_app_lock_signout_alert_title), title = stringResource(id = R.string.screen_app_lock_signout_alert_title),
content = stringResource(id = R.string.screen_app_lock_signout_alert_message), content = stringResource(id = R.string.screen_app_lock_signout_alert_message),
onDismiss = onSignOut, onDismiss = onSignOut,
modifier = modifier,
) )
} }
} }
@ -258,9 +254,11 @@ private fun PinUnlockExpandedView(
@Composable @Composable
private fun PinDotsRow( private fun PinDotsRow(
pinEntry: PinEntry, pinEntry: PinEntry,
modifier: Modifier = Modifier,
) { ) {
Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { Row(
horizontalArrangement = spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
for (digit in pinEntry.digits) { for (digit in pinEntry.digits) {
PinDot(isFilled = digit is PinDigit.Filled) PinDot(isFilled = digit is PinDigit.Filled)
} }
@ -270,7 +268,6 @@ private fun PinDotsRow(
@Composable @Composable
private fun PinDot( private fun PinDot(
isFilled: Boolean, isFilled: Boolean,
modifier: Modifier = Modifier,
) { ) {
val backgroundColor = if (isFilled) { val backgroundColor = if (isFilled) {
ElementTheme.colors.iconPrimary ElementTheme.colors.iconPrimary
@ -278,7 +275,7 @@ private fun PinDot(
ElementTheme.colors.bgSubtlePrimary ElementTheme.colors.bgSubtlePrimary
} }
Box( Box(
modifier = modifier modifier = Modifier
.size(14.dp) .size(14.dp)
.background(backgroundColor, CircleShape) .background(backgroundColor, CircleShape)
) )
@ -290,7 +287,10 @@ private fun PinUnlockHeader(
isInAppUnlock: Boolean, isInAppUnlock: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (isInAppUnlock) { if (isInAppUnlock) {
RoundedIconAtom(imageVector = Icons.Filled.Lock) RoundedIconAtom(imageVector = Icons.Filled.Lock)
} else { } else {

View file

@ -108,14 +108,13 @@ private fun PinKeypadRow(
models: ImmutableList<PinKeypadModel>, models: ImmutableList<PinKeypadModel>,
onClick: (PinKeypadModel) -> Unit, onClick: (PinKeypadModel) -> Unit,
pinKeySize: Dp, pinKeySize: Dp,
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top, verticalAlignment: Alignment.Vertical = Alignment.Top,
) { ) {
Row( Row(
horizontalArrangement = horizontalArrangement, horizontalArrangement = horizontalArrangement,
verticalAlignment = verticalAlignment, verticalAlignment = verticalAlignment,
modifier = modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
val commonModifier = Modifier.size(pinKeySize) val commonModifier = Modifier.size(pinKeySize)
for (model in models) { for (model in models) {

View file

@ -76,7 +76,7 @@ fun AccountProviderView(
} else { } else {
RoundedIconAtom( RoundedIconAtom(
size = RoundedIconAtomSize.Medium, size = RoundedIconAtomSize.Medium,
imageVector = CompoundIcons.Search, imageVector = CompoundIcons.Search(),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} }

View file

@ -179,7 +179,6 @@ private fun LoginForm(
state: LoginPasswordState, state: LoginPasswordState,
isLoading: Boolean, isLoading: Boolean,
onSubmit: () -> Unit, onSubmit: () -> Unit,
modifier: Modifier = Modifier
) { ) {
var loginFieldState by textFieldState(stateValue = state.formState.login) var loginFieldState by textFieldState(stateValue = state.formState.login)
var passwordFieldState by textFieldState(stateValue = state.formState.password) var passwordFieldState by textFieldState(stateValue = state.formState.password)
@ -187,7 +186,7 @@ private fun LoginForm(
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val eventSink = state.eventSink val eventSink = state.eventSink
Column(modifier) { Column {
Text( Text(
text = stringResource(R.string.screen_login_form_header), text = stringResource(R.string.screen_login_form_header),
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = 16.dp),
@ -228,7 +227,7 @@ private fun LoginForm(
IconButton(onClick = { IconButton(onClick = {
loginFieldState = "" loginFieldState = ""
}) { }) {
Icon(imageVector = CompoundIcons.Close, contentDescription = stringResource(CommonStrings.action_clear)) Icon(imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_clear))
} }
} }
} else { } else {
@ -264,7 +263,7 @@ private fun LoginForm(
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = { trailingIcon = {
val image = val image =
if (passwordVisible) CompoundIcons.VisibilityOn else CompoundIcons.VisibilityOff if (passwordVisible) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff()
val description = val description =
if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password) if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password)

View file

@ -103,7 +103,7 @@ fun SearchAccountProviderView(
item { item {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp), modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp),
iconImageVector = CompoundIcons.Search, iconImageVector = CompoundIcons.Search(),
title = stringResource(id = R.string.screen_account_provider_form_title), title = stringResource(id = R.string.screen_account_provider_form_title),
subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle), subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle),
) )
@ -139,7 +139,7 @@ fun SearchAccountProviderView(
eventSink(SearchAccountProviderEvents.UserInput("")) eventSink(SearchAccountProviderEvents.UserInput(""))
}) { }) {
Icon( Icon(
imageVector = CompoundIcons.Close, imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_clear) contentDescription = stringResource(CommonStrings.action_clear)
) )
} }

View file

@ -25,11 +25,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.login.impl.R import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.isWaitListError import io.element.android.features.login.impl.error.isWaitListError
import io.element.android.features.login.impl.error.loginError import io.element.android.features.login.impl.error.loginError
@ -119,11 +119,10 @@ private fun WaitListContent(
private fun OverallContent( private fun OverallContent(
state: WaitListState, state: WaitListState,
onCancelClicked: () -> Unit, onCancelClicked: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
if (state.loginAction !is AsyncData.Success) { if (state.loginAction !is AsyncData.Success) {
CompositionLocalProvider(LocalContentColor provides Color.Black) { CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textOnSolidPrimary) {
TextButton( TextButton(
text = stringResource(CommonStrings.action_cancel), text = stringResource(CommonStrings.action_cancel),
onClick = onCancelClicked, onClick = onCancelClicked,

View file

@ -24,12 +24,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.logout.impl.tools.isBackingUp import io.element.android.features.logout.impl.tools.isBackingUp
import io.element.android.features.logout.impl.ui.LogoutActionDialog import io.element.android.features.logout.impl.ui.LogoutActionDialog
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -41,7 +40,6 @@ import io.element.android.libraries.designsystem.theme.components.LinearProgress
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
@ -64,7 +62,7 @@ fun LogoutView(
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
title = title(state), title = title(state),
subTitle = subtitle(state), subTitle = subtitle(state),
iconVector = ImageVector.vectorResource(CommonDrawables.ic_key), iconVector = CompoundIcons.KeySolid(),
modifier = modifier, modifier = modifier,
content = { Content(state) }, content = { Content(state) },
buttons = { buttons = {
@ -86,7 +84,7 @@ fun LogoutView(
onForceLogoutClicked = { onForceLogoutClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = true)) eventSink(LogoutEvents.Logout(ignoreSdkError = true))
}, },
onDismissError = { onDismissDialog = {
eventSink(LogoutEvents.CloseDialogs) eventSink(LogoutEvents.CloseDialogs)
}, },
onSuccessLogout = { onSuccessLogout = {

View file

@ -41,7 +41,7 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
onForceLogoutClicked = { onForceLogoutClicked = {
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true)) eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true))
}, },
onDismissError = { onDismissDialog = {
eventSink(DirectLogoutEvents.CloseDialogs) eventSink(DirectLogoutEvents.CloseDialogs)
}, },
onSuccessLogout = { onSuccessLogout = {

View file

@ -32,8 +32,7 @@ fun LogoutActionDialog(
state: AsyncAction<String?>, state: AsyncAction<String?>,
onConfirmClicked: () -> Unit, onConfirmClicked: () -> Unit,
onForceLogoutClicked: () -> Unit, onForceLogoutClicked: () -> Unit,
// TODO Rename onDismissDialog: () -> Unit,
onDismissError: () -> Unit,
onSuccessLogout: (String?) -> Unit, onSuccessLogout: (String?) -> Unit,
) { ) {
when (state) { when (state) {
@ -42,7 +41,7 @@ fun LogoutActionDialog(
AsyncAction.Confirming -> AsyncAction.Confirming ->
LogoutConfirmationDialog( LogoutConfirmationDialog(
onSubmitClicked = onConfirmClicked, onSubmitClicked = onConfirmClicked,
onDismiss = onDismissError onDismiss = onDismissDialog
) )
is AsyncAction.Loading -> is AsyncAction.Loading ->
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
@ -52,7 +51,7 @@ fun LogoutActionDialog(
content = stringResource(id = CommonStrings.error_unknown), content = stringResource(id = CommonStrings.error_unknown),
retryText = stringResource(id = CommonStrings.action_signout_anyway), retryText = stringResource(id = CommonStrings.action_signout_anyway),
onRetry = onForceLogoutClicked, onRetry = onForceLogoutClicked,
onDismiss = onDismissError, onDismiss = onDismissDialog,
) )
is AsyncAction.Success -> { is AsyncAction.Success -> {
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout) val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)

View file

@ -62,6 +62,7 @@ dependencies {
implementation(projects.libraries.voicerecorder.api) implementation(projects.libraries.voicerecorder.api)
implementation(projects.libraries.mediaplayer.api) implementation(projects.libraries.mediaplayer.api)
implementation(projects.libraries.uiUtils) implementation(projects.libraries.uiUtils)
implementation(projects.libraries.testtags)
implementation(projects.features.networkmonitor.api) implementation(projects.features.networkmonitor.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(libs.coil.compose) implementation(libs.coil.compose)

View file

@ -59,6 +59,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent 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.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
@ -97,6 +98,7 @@ class MessagesPresenter @AssistedInject constructor(
private val composerPresenter: MessageComposerPresenter, private val composerPresenter: MessageComposerPresenter,
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter,
timelinePresenterFactory: TimelinePresenter.Factory, timelinePresenterFactory: TimelinePresenter.Factory,
private val typingNotificationPresenter: TypingNotificationPresenter,
private val actionListPresenter: ActionListPresenter, private val actionListPresenter: ActionListPresenter,
private val customReactionPresenter: CustomReactionPresenter, private val customReactionPresenter: CustomReactionPresenter,
private val reactionSummaryPresenter: ReactionSummaryPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter,
@ -129,6 +131,7 @@ class MessagesPresenter @AssistedInject constructor(
val composerState = composerPresenter.present() val composerState = composerPresenter.present()
val voiceMessageComposerState = voiceMessageComposerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present()
val timelineState = timelinePresenter.present() val timelineState = timelinePresenter.present()
val typingNotificationState = typingNotificationPresenter.present()
val actionListState = actionListPresenter.present() val actionListState = actionListPresenter.present()
val customReactionState = customReactionPresenter.present() val customReactionState = customReactionPresenter.present()
val reactionSummaryState = reactionSummaryPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present()
@ -155,6 +158,14 @@ class MessagesPresenter @AssistedInject constructor(
mutableStateOf(false) mutableStateOf(false)
} }
LaunchedEffect(Unit) {
// Remove the unread flag on entering but don't send read receipts
// as those will be handled by the timeline.
withContext(dispatchers.io) {
room.setUnreadFlag(isUnread = false)
}
}
LaunchedEffect(syncUpdateFlow.value) { LaunchedEffect(syncUpdateFlow.value) {
withContext(dispatchers.io) { withContext(dispatchers.io) {
canJoinCall = room.canUserJoinCall(room.sessionId).getOrDefault(false) canJoinCall = room.canUserJoinCall(room.sessionId).getOrDefault(false)
@ -225,6 +236,7 @@ class MessagesPresenter @AssistedInject constructor(
composerState = composerState, composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState, voiceMessageComposerState = voiceMessageComposerState,
timelineState = timelineState, timelineState = timelineState,
typingNotificationState = typingNotificationState,
actionListState = actionListState, actionListState = actionListState,
customReactionState = customReactionState, customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState, reactionSummaryState = reactionSummaryState,

View file

@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.components.customreact
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
@ -42,6 +43,7 @@ data class MessagesState(
val composerState: MessageComposerState, val composerState: MessageComposerState,
val voiceMessageComposerState: VoiceMessageComposerState, val voiceMessageComposerState: VoiceMessageComposerState,
val timelineState: TimelineState, val timelineState: TimelineState,
val typingNotificationState: TypingNotificationState,
val actionListState: ActionListState, val actionListState: ActionListState,
val customReactionState: CustomReactionState, val customReactionState: CustomReactionState,
val reactionSummaryState: ReactionSummaryState, val reactionSummaryState: ReactionSummaryState,

View file

@ -17,16 +17,26 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.anActionListState
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineItemList
import io.element.android.features.messages.impl.timeline.aTimelineState import io.element.android.features.messages.impl.timeline.aTimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.aRetrySendMenuState
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.typing.aTypingNotificationState
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -42,87 +52,120 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
override val values: Sequence<MessagesState> override val values: Sequence<MessagesState>
get() = sequenceOf( get() = sequenceOf(
aMessagesState(), aMessagesState(),
aMessagesState().copy(hasNetworkConnection = false), aMessagesState(hasNetworkConnection = false),
aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)), aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
aMessagesState().copy(userHasPermissionToSendMessage = false), aMessagesState(userHasPermissionToSendMessage = false),
aMessagesState().copy(showReinvitePrompt = true), aMessagesState(showReinvitePrompt = true),
aMessagesState().copy( aMessagesState(
roomName = AsyncData.Uninitialized, roomName = AsyncData.Uninitialized,
roomAvatar = AsyncData.Uninitialized, roomAvatar = AsyncData.Uninitialized,
), ),
aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)), aMessagesState(composerState = aMessageComposerState(showTextFormatting = true)),
aMessagesState().copy( aMessagesState(
enableVoiceMessages = true, enableVoiceMessages = true,
voiceMessageComposerState = aVoiceMessageComposerState(showPermissionRationaleDialog = true), voiceMessageComposerState = aVoiceMessageComposerState(showPermissionRationaleDialog = true),
), ),
aMessagesState().copy( aMessagesState(
composerState = aMessageComposerState().copy( composerState = aMessageComposerState(
attachmentsState = AttachmentsState.Sending.Processing(persistentListOf()) attachmentsState = AttachmentsState.Sending.Processing(persistentListOf())
), ),
), ),
aMessagesState().copy( aMessagesState(
composerState = aMessageComposerState().copy( composerState = aMessageComposerState(
attachmentsState = AttachmentsState.Sending.Uploading(0.33f) attachmentsState = AttachmentsState.Sending.Uploading(0.33f)
), ),
), ),
aMessagesState().copy( aMessagesState(
callState = RoomCallState.ONGOING, callState = RoomCallState.ONGOING,
), ),
aMessagesState().copy( aMessagesState(
enableVoiceMessages = true, enableVoiceMessages = true,
voiceMessageComposerState = aVoiceMessageComposerState( voiceMessageComposerState = aVoiceMessageComposerState(
voiceMessageState = aVoiceMessagePreviewState(), voiceMessageState = aVoiceMessagePreviewState(),
showSendFailureDialog = true showSendFailureDialog = true
), ),
), ),
aMessagesState().copy( aMessagesState(
callState = RoomCallState.DISABLED, callState = RoomCallState.DISABLED,
), ),
) )
} }
fun aMessagesState() = MessagesState( fun aMessagesState(
roomId = RoomId("!id:domain"), roomName: AsyncData<String> = AsyncData.Success("Room name"),
roomName = AsyncData.Success("Room name"), roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)), userHasPermissionToSendMessage: Boolean = true,
userHasPermissionToSendMessage = true, userHasPermissionToRedactOwn: Boolean = false,
userHasPermissionToRedactOwn = false, userHasPermissionToRedactOther: Boolean = false,
userHasPermissionToRedactOther = false, userHasPermissionToSendReaction: Boolean = true,
userHasPermissionToSendReaction = true, composerState: MessageComposerState = aMessageComposerState(
composerState = aMessageComposerState().copy(
richTextEditorState = RichTextEditorState("Hello", initialFocus = true), richTextEditorState = RichTextEditorState("Hello", initialFocus = true),
isFullScreen = false, isFullScreen = false,
mode = MessageComposerMode.Normal, mode = MessageComposerMode.Normal,
), ),
voiceMessageComposerState = aVoiceMessageComposerState(), voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
timelineState = aTimelineState().copy( timelineState: TimelineState = aTimelineState(
timelineItems = aTimelineItemList(aTimelineItemTextContent()), timelineItems = aTimelineItemList(aTimelineItemTextContent()),
), ),
retrySendMenuState = RetrySendMenuState( retrySendMenuState: RetrySendMenuState = aRetrySendMenuState(),
selectedEvent = null, readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
eventSink = {}, actionListState: ActionListState = anActionListState(),
), customReactionState: CustomReactionState = aCustomReactionState(),
readReceiptBottomSheetState = ReadReceiptBottomSheetState( reactionSummaryState: ReactionSummaryState = aReactionSummaryState(),
selectedEvent = null, hasNetworkConnection: Boolean = true,
eventSink = {}, showReinvitePrompt: Boolean = false,
), enableVoiceMessages: Boolean = true,
actionListState = anActionListState(), callState: RoomCallState = RoomCallState.ENABLED,
customReactionState = CustomReactionState( eventSink: (MessagesEvents) -> Unit = {},
target = CustomReactionState.Target.None, ) = MessagesState(
eventSink = {}, roomId = RoomId("!id:domain"),
selectedEmoji = persistentSetOf(), roomName = roomName,
), roomAvatar = roomAvatar,
reactionSummaryState = ReactionSummaryState( userHasPermissionToSendMessage = userHasPermissionToSendMessage,
target = null, userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
eventSink = {}, userHasPermissionToRedactOther = userHasPermissionToRedactOther,
), userHasPermissionToSendReaction = userHasPermissionToSendReaction,
hasNetworkConnection = true, composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState,
typingNotificationState = aTypingNotificationState(),
timelineState = timelineState,
retrySendMenuState = retrySendMenuState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
actionListState = actionListState,
customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = null, snackbarMessage = null,
inviteProgress = AsyncData.Uninitialized, inviteProgress = AsyncData.Uninitialized,
showReinvitePrompt = false, showReinvitePrompt = showReinvitePrompt,
enableTextFormatting = true, enableTextFormatting = true,
enableVoiceMessages = true, enableVoiceMessages = enableVoiceMessages,
callState = RoomCallState.ENABLED, callState = callState,
appName = "Element", appName = "Element",
eventSink = {} eventSink = eventSink,
)
fun aReactionSummaryState(
target: ReactionSummaryState.Summary? = null,
eventSink: (ReactionSummaryEvents) -> Unit = {}
) = ReactionSummaryState(
target = target,
eventSink = eventSink,
)
fun aCustomReactionState(
target: CustomReactionState.Target = CustomReactionState.Target.None,
eventSink: (CustomReactionEvents) -> Unit = {},
) = CustomReactionState(
target = target,
selectedEmoji = persistentSetOf(),
eventSink = eventSink,
)
fun aReadReceiptBottomSheetState(
selectedEvent: TimelineItem.Event? = null,
eventSink: (ReadReceiptBottomSheetEvents) -> Unit = {},
) = ReadReceiptBottomSheetState(
selectedEvent = selectedEvent,
eventSink = eventSink,
) )

View file

@ -39,7 +39,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -102,7 +101,6 @@ 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.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
@ -126,9 +124,8 @@ fun MessagesView(
onCreatePollClicked: () -> Unit, onCreatePollClicked: () -> Unit,
onJoinCallClicked: () -> Unit, onJoinCallClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false
) { ) {
LogCompositions(tag = "MessagesScreen", msg = "Root")
OnLifecycleEvent { _, event -> OnLifecycleEvent { _, event ->
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event)) state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
} }
@ -146,8 +143,6 @@ fun MessagesView(
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose // This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
val localView = LocalView.current val localView = LocalView.current
LogCompositions(tag = "MessagesScreen", msg = "Content")
fun onMessageClicked(event: TimelineItem.Event) { fun onMessageClicked(event: TimelineItem.Event) {
Timber.v("OnMessageClicked= ${event.id}") Timber.v("OnMessageClicked= ${event.id}")
val hideKeyboard = onEventClicked(event) val hideKeyboard = onEventClicked(event)
@ -198,7 +193,12 @@ fun MessagesView(
roomName = state.roomName.dataOrNull(), roomName = state.roomName.dataOrNull(),
roomAvatar = state.roomAvatar.dataOrNull(), roomAvatar = state.roomAvatar.dataOrNull(),
callState = state.callState, callState = state.callState,
onBackPressed = onBackPressed, onBackPressed = {
// Since the textfield is now based on an Android view, this is no longer done automatically.
// We need to hide the keyboard when navigating out of this screen.
localView.hideKeyboard()
onBackPressed()
},
onRoomDetailsClicked = onRoomDetailsClicked, onRoomDetailsClicked = onRoomDetailsClicked,
onJoinCallClicked = onJoinCallClicked, onJoinCallClicked = onJoinCallClicked,
) )
@ -229,6 +229,7 @@ fun MessagesView(
onSwipeToReply = { targetEvent -> onSwipeToReply = { targetEvent ->
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent)) state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
}, },
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
) )
}, },
snackbarHost = { snackbarHost = {
@ -253,7 +254,6 @@ fun MessagesView(
state = state.customReactionState, state = state.customReactionState,
onEmojiSelected = { eventId, emoji -> onEmojiSelected = { eventId, emoji ->
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId)) state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
} }
) )
@ -264,14 +264,6 @@ fun MessagesView(
onUserDataClicked = onUserDataClicked, onUserDataClicked = onUserDataClicked,
) )
ReinviteDialog(state = state) ReinviteDialog(state = state)
// Since the textfield is now based on an Android view, this is no longer done automatically.
// We need to hide the keyboard automatically when navigating out of this screen.
DisposableEffect(Unit) {
onDispose {
localView.hideKeyboard()
}
}
} }
@Composable @Composable
@ -329,6 +321,7 @@ private fun MessagesViewContent(
onTimestampClicked: (TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit,
onSendLocationClicked: () -> Unit, onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit, onCreatePollClicked: () -> Unit,
forceJumpToBottomVisibility: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onSwipeToReply: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit,
) { ) {
@ -389,6 +382,7 @@ private fun MessagesViewContent(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
state = state.timelineState, state = state.timelineState,
roomName = state.roomName.dataOrNull(), roomName = state.roomName.dataOrNull(),
typingNotificationState = state.typingNotificationState,
onMessageClicked = onMessageClicked, onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked, onMessageLongClicked = onMessageLongClicked,
onUserDataClicked = onUserDataClicked, onUserDataClicked = onUserDataClicked,
@ -398,6 +392,7 @@ private fun MessagesViewContent(
onMoreReactionsClicked = onMoreReactionsClicked, onMoreReactionsClicked = onMoreReactionsClicked,
onReadReceiptClick = onReadReceiptClick, onReadReceiptClick = onReadReceiptClick,
onSwipeToReply = onSwipeToReply, onSwipeToReply = onSwipeToReply,
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
) )
}, },
sheetContent = { subcomposing: Boolean -> sheetContent = { subcomposing: Boolean ->
@ -417,10 +412,9 @@ private fun MessagesViewContent(
private fun MessagesViewComposerBottomSheetContents( private fun MessagesViewComposerBottomSheetContents(
subcomposing: Boolean, subcomposing: Boolean,
state: MessagesState, state: MessagesState,
modifier: Modifier = Modifier,
) { ) {
if (state.userHasPermissionToSendMessage) { if (state.userHasPermissionToSendMessage) {
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
MentionSuggestionsPickerView( MentionSuggestionsPickerView(
modifier = Modifier modifier = Modifier
.heightIn(max = 230.dp) .heightIn(max = 230.dp)
@ -448,7 +442,7 @@ private fun MessagesViewComposerBottomSheetContents(
) )
} }
} else { } else {
CantSendMessageBanner(modifier = modifier) CantSendMessageBanner()
} }
} }
@ -461,10 +455,8 @@ private fun MessagesViewTopBar(
onRoomDetailsClicked: () -> Unit, onRoomDetailsClicked: () -> Unit,
onJoinCallClicked: () -> Unit, onJoinCallClicked: () -> Unit,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
TopAppBar( TopAppBar(
modifier = modifier,
navigationIcon = { navigationIcon = {
BackButton(onClick = onBackPressed) BackButton(onClick = onBackPressed)
}, },
@ -489,7 +481,7 @@ private fun MessagesViewTopBar(
} else { } else {
IconButton(onClick = onJoinCallClicked, enabled = callState != RoomCallState.DISABLED) { IconButton(onClick = onJoinCallClicked, enabled = callState != RoomCallState.DISABLED) {
Icon( Icon(
imageVector = CompoundIcons.VideoCallSolid, imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call), contentDescription = stringResource(CommonStrings.a11y_start_call),
) )
} }
@ -502,7 +494,6 @@ private fun MessagesViewTopBar(
@Composable @Composable
private fun JoinCallMenuItem( private fun JoinCallMenuItem(
modifier: Modifier = Modifier,
onJoinCallClicked: () -> Unit, onJoinCallClicked: () -> Unit,
) { ) {
Material3Button( Material3Button(
@ -512,11 +503,11 @@ private fun JoinCallMenuItem(
containerColor = ElementTheme.colors.iconAccentTertiary containerColor = ElementTheme.colors.iconAccentTertiary
), ),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
modifier = modifier.heightIn(min = 36.dp), modifier = Modifier.heightIn(min = 36.dp),
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.VideoCallSolid, imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = null contentDescription = null
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
@ -550,11 +541,9 @@ private fun RoomAvatarAndNameRow(
} }
@Composable @Composable
private fun CantSendMessageBanner( private fun CantSendMessageBanner() {
modifier: Modifier = Modifier,
) {
Row( Row(
modifier = modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.secondary) .background(MaterialTheme.colorScheme.secondary)
.padding(16.dp), .padding(16.dp),
@ -584,5 +573,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onSendLocationClicked = {}, onSendLocationClicked = {},
onCreatePollClicked = {}, onCreatePollClicked = {},
onJoinCallClicked = {}, onJoinCallClicked = {},
forceJumpToBottomVisibility = true,
) )
} }

View file

@ -122,9 +122,12 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
} }
} }
fun anActionListState() = ActionListState( fun anActionListState(
target = ActionListState.Target.None, target: ActionListState.Target = ActionListState.Target.None,
eventSink = {} eventSink: (ActionListEvents) -> Unit = {},
) = ActionListState(
target = target,
eventSink = eventSink
) )
fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> { fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {

View file

@ -53,6 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
@ -83,7 +84,6 @@ import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.hide import io.element.android.libraries.designsystem.theme.components.hide
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -316,7 +316,7 @@ private fun EmojiReactionsRow(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
resourceId = CommonDrawables.ic_add_reaction, imageVector = CompoundIcons.ReactionAdd(),
contentDescription = stringResource(id = CommonStrings.a11y_react_with_other_emojis), contentDescription = stringResource(id = CommonStrings.a11y_react_with_other_emojis),
tint = MaterialTheme.colorScheme.secondary, tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier modifier = Modifier
@ -337,7 +337,6 @@ private fun EmojiButton(
emoji: String, emoji: String,
isHighlighted: Boolean, isHighlighted: Boolean,
onClicked: (String) -> Unit, onClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val backgroundColor = if (isHighlighted) { val backgroundColor = if (isHighlighted) {
ElementTheme.colors.bgActionPrimaryRest ElementTheme.colors.bgActionPrimaryRest
@ -350,7 +349,7 @@ private fun EmojiButton(
stringResource(id = CommonStrings.a11y_react_with, emoji) stringResource(id = CommonStrings.a11y_react_with, emoji)
} }
Box( Box(
modifier = modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background(backgroundColor, CircleShape) .background(backgroundColor, CircleShape)
.clearAndSetSemantics { .clearAndSetSemantics {

View file

@ -29,13 +29,13 @@ sealed class TimelineItemAction(
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
val destructive: Boolean = false val destructive: Boolean = false
) { ) {
data object Forward : TimelineItemAction(CommonStrings.action_forward, CommonDrawables.ic_forward) data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
data object Copy : TimelineItemAction(CommonStrings.action_copy, CommonDrawables.ic_copy) data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy)
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_delete, destructive = true) data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
data object Reply : TimelineItemAction(CommonStrings.action_reply, CommonDrawables.ic_reply) data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CommonDrawables.ic_reply) data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply)
data object Edit : TimelineItemAction(CommonStrings.action_edit, CommonDrawables.ic_edit_outline) data object Edit : TimelineItemAction(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit)
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options) data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_chat_problem, destructive = true) data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_polls_end) data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
} }

View file

@ -118,10 +118,9 @@ private fun AttachmentPreviewContent(
attachment: Attachment, attachment: Attachment,
onSendClicked: () -> Unit, onSendClicked: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = 24.dp) .padding(top = 24.dp)
) { ) {

View file

@ -43,7 +43,6 @@ import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -101,34 +100,33 @@ private fun AttachmentSourcePickerMenu(
onSendLocationClicked: () -> Unit, onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit, onCreatePollClicked: () -> Unit,
enableTextFormatting: Boolean, enableTextFormatting: Boolean,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier modifier = Modifier
.navigationBarsPadding() .navigationBarsPadding()
.imePadding() .imePadding()
) { ) {
ListItem( ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }, modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) },
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_image)), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Image())),
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) }, headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) },
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
) )
ListItem( ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }, modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) },
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_attachment)), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Attachment())),
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_files)) }, headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_files)) },
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
) )
ListItem( ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) }, modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) },
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_take_photo_camera)), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.TakePhoto())),
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) }, headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) },
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
) )
ListItem( ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) }, modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VideoCall)), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VideoCall())),
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) }, headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
) )
@ -138,7 +136,7 @@ private fun AttachmentSourcePickerMenu(
state.eventSink(MessageComposerEvents.PickAttachmentSource.Location) state.eventSink(MessageComposerEvents.PickAttachmentSource.Location)
onSendLocationClicked() onSendLocationClicked()
}, },
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_location_pin)), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.LocationPin())),
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_location)) }, headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
) )
@ -149,7 +147,7 @@ private fun AttachmentSourcePickerMenu(
state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll) state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll)
onCreatePollClicked() onCreatePollClicked()
}, },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Polls)), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Polls())),
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
) )
@ -157,7 +155,7 @@ private fun AttachmentSourcePickerMenu(
if (enableTextFormatting) { if (enableTextFormatting) {
ListItem( ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) }, modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_text_formatting, null)), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.TextFormatting())),
headlineContent = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) }, headlineContent = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
) )

View file

@ -32,7 +32,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos
} }
fun aMessageComposerState( fun aMessageComposerState(
composerState: RichTextEditorState = RichTextEditorState(""), richTextEditorState: RichTextEditorState = RichTextEditorState(""),
isFullScreen: Boolean = false, isFullScreen: Boolean = false,
mode: MessageComposerMode = MessageComposerMode.Normal, mode: MessageComposerMode = MessageComposerMode.Normal,
showTextFormatting: Boolean = false, showTextFormatting: Boolean = false,
@ -42,7 +42,7 @@ fun aMessageComposerState(
attachmentsState: AttachmentsState = AttachmentsState.None, attachmentsState: AttachmentsState = AttachmentsState.None,
memberSuggestions: ImmutableList<MentionSuggestion> = persistentListOf(), memberSuggestions: ImmutableList<MentionSuggestion> = persistentListOf(),
) = MessageComposerState( ) = MessageComposerState(
richTextEditorState = composerState, richTextEditorState = richTextEditorState,
isFullScreen = isFullScreen, isFullScreen = isFullScreen,
mode = mode, mode = mode,
showTextFormatting = showTextFormatting, showTextFormatting = showTextFormatting,

View file

@ -22,7 +22,6 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -90,7 +89,6 @@ class TimelinePresenter @AssistedInject constructor(
mutableStateOf(null) mutableStateOf(null)
} }
val lastReadReceiptIndex = rememberSaveable { mutableIntStateOf(Int.MAX_VALUE) }
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) } val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
val timelineItems by timelineItemsFactory.collectItemsAsState() val timelineItems by timelineItemsFactory.collectItemsAsState()
@ -128,7 +126,6 @@ class TimelinePresenter @AssistedInject constructor(
appScope.sendReadReceiptIfNeeded( appScope.sendReadReceiptIfNeeded(
firstVisibleIndex = event.firstIndex, firstVisibleIndex = event.firstIndex,
timelineItems = timelineItems, timelineItems = timelineItems,
lastReadReceiptIndex = lastReadReceiptIndex,
lastReadReceiptId = lastReadReceiptId, lastReadReceiptId = lastReadReceiptId,
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE, readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
) )
@ -228,16 +225,19 @@ class TimelinePresenter @AssistedInject constructor(
private fun CoroutineScope.sendReadReceiptIfNeeded( private fun CoroutineScope.sendReadReceiptIfNeeded(
firstVisibleIndex: Int, firstVisibleIndex: Int,
timelineItems: ImmutableList<TimelineItem>, timelineItems: ImmutableList<TimelineItem>,
lastReadReceiptIndex: MutableState<Int>,
lastReadReceiptId: MutableState<EventId?>, lastReadReceiptId: MutableState<EventId?>,
readReceiptType: ReceiptType, readReceiptType: ReceiptType,
) = launch(dispatchers.computation) { ) = launch(dispatchers.computation) {
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item // If we are at the bottom of timeline, we mark the room as read.
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) if (firstVisibleIndex == 0) {
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) { room.markAsRead(receiptType = readReceiptType)
lastReadReceiptIndex.value = firstVisibleIndex } else {
lastReadReceiptId.value = eventId // Get last valid EventId seen by the user, as the first index might refer to a Virtual item
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
if (eventId != null && eventId != lastReadReceiptId.value) {
lastReadReceiptId.value = eventId
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
}
} }
} }

View file

@ -48,12 +48,14 @@ import kotlin.random.Random
fun aTimelineState( fun aTimelineState(
timelineItems: ImmutableList<TimelineItem> = persistentListOf(), timelineItems: ImmutableList<TimelineItem> = persistentListOf(),
paginationState: MatrixTimeline.PaginationState = aPaginationState(), paginationState: MatrixTimeline.PaginationState = aPaginationState(),
renderReadReceipts: Boolean = false,
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
eventSink: (TimelineEvents) -> Unit = {}, eventSink: (TimelineEvents) -> Unit = {},
) = TimelineState( ) = TimelineState(
timelineItems = timelineItems, timelineItems = timelineItems,
timelineRoomInfo = aTimelineRoomInfo(), timelineRoomInfo = timelineRoomInfo,
paginationState = paginationState, paginationState = paginationState,
renderReadReceipts = false, renderReadReceipts = renderReadReceipts,
highlightedEventId = null, highlightedEventId = null,
newEventState = NewEventState.None, newEventState = NewEventState.None,
sessionState = aSessionState( sessionState = aSessionState(
@ -196,9 +198,11 @@ internal fun aTimelineItemDebugInfo(
latestEditedJson latestEditedJson
) )
internal fun aTimelineItemReadReceipts(): TimelineItemReadReceipts { internal fun aTimelineItemReadReceipts(
receipts: List<ReadReceiptData> = emptyList(),
): TimelineItemReadReceipts {
return TimelineItemReadReceipts( return TimelineItemReadReceipts(
receipts = emptyList<ReadReceiptData>().toImmutableList(), receipts = receipts.toImmutableList(),
) )
} }
@ -232,8 +236,9 @@ internal fun aGroupedEvents(
internal fun aTimelineRoomInfo( internal fun aTimelineRoomInfo(
isDirect: Boolean = false, isDirect: Boolean = false,
userHasPermissionToSendMessage: Boolean = true,
) = TimelineRoomInfo( ) = TimelineRoomInfo(
isDirect = isDirect, isDirect = isDirect,
userHasPermissionToSendMessage = true, userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = true, userHasPermissionToSendReaction = true,
) )

View file

@ -47,7 +47,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -62,6 +61,9 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.messages.impl.typing.TypingNotificationView
import io.element.android.features.messages.impl.typing.aTypingNotificationState
import io.element.android.libraries.designsystem.animation.alphaAnimation import io.element.android.libraries.designsystem.animation.alphaAnimation
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -75,6 +77,7 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun TimelineView( fun TimelineView(
state: TimelineState, state: TimelineState,
typingNotificationState: TypingNotificationState,
roomName: String?, roomName: String?,
onUserDataClicked: (UserId) -> Unit, onUserDataClicked: (UserId) -> Unit,
onMessageClicked: (TimelineItem.Event) -> Unit, onMessageClicked: (TimelineItem.Event) -> Unit,
@ -86,6 +89,7 @@ fun TimelineView(
onMoreReactionsClicked: (TimelineItem.Event) -> Unit, onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false
) { ) {
fun onReachedLoadMore() { fun onReachedLoadMore() {
state.eventSink(TimelineEvents.LoadMore) state.eventSink(TimelineEvents.LoadMore)
@ -112,6 +116,9 @@ fun TimelineView(
reverseLayout = true, reverseLayout = true,
contentPadding = PaddingValues(vertical = 8.dp), contentPadding = PaddingValues(vertical = 8.dp),
) { ) {
item {
TypingNotificationView(state = typingNotificationState)
}
items( items(
items = state.timelineItems, items = state.timelineItems,
contentType = { timelineItem -> timelineItem.contentType() }, contentType = { timelineItem -> timelineItem.contentType() },
@ -157,6 +164,7 @@ fun TimelineView(
TimelineScrollHelper( TimelineScrollHelper(
isTimelineEmpty = state.timelineItems.isEmpty(), isTimelineEmpty = state.timelineItems.isEmpty(),
lazyListState = lazyListState, lazyListState = lazyListState,
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
newEventState = state.newEventState, newEventState = state.newEventState,
onScrollFinishedAt = ::onScrollFinishedAt onScrollFinishedAt = ::onScrollFinishedAt
) )
@ -168,6 +176,7 @@ private fun BoxScope.TimelineScrollHelper(
isTimelineEmpty: Boolean, isTimelineEmpty: Boolean,
lazyListState: LazyListState, lazyListState: LazyListState,
newEventState: NewEventState, newEventState: NewEventState,
forceJumpToBottomVisibility: Boolean,
onScrollFinishedAt: (Int) -> Unit, onScrollFinishedAt: (Int) -> Unit,
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -205,7 +214,7 @@ private fun BoxScope.TimelineScrollHelper(
JumpToBottomButton( JumpToBottomButton(
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered // Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
isVisible = !canAutoScroll, isVisible = !canAutoScroll || forceJumpToBottomVisibility,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp), .padding(end = 24.dp, bottom = 12.dp),
@ -221,7 +230,7 @@ private fun JumpToBottomButton(
) { ) {
AnimatedVisibility( AnimatedVisibility(
modifier = modifier, modifier = modifier,
visible = isVisible || LocalInspectionMode.current, visible = isVisible,
enter = scaleIn(animationSpec = tween(100)), enter = scaleIn(animationSpec = tween(100)),
exit = scaleOut(animationSpec = tween(100)), exit = scaleOut(animationSpec = tween(100)),
) { ) {
@ -237,7 +246,7 @@ private fun JumpToBottomButton(
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.rotate(90f), .rotate(90f),
imageVector = CompoundIcons.ArrowRight, imageVector = CompoundIcons.ArrowRight(),
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom) contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
) )
} }
@ -256,6 +265,7 @@ internal fun TimelineViewPreview(
TimelineView( TimelineView(
state = aTimelineState(timelineItems), state = aTimelineState(timelineItems),
roomName = null, roomName = null,
typingNotificationState = aTypingNotificationState(),
onMessageClicked = {}, onMessageClicked = {},
onTimestampClicked = {}, onTimestampClicked = {},
onUserDataClicked = {}, onUserDataClicked = {},
@ -265,6 +275,7 @@ internal fun TimelineViewPreview(
onMoreReactionsClicked = {}, onMoreReactionsClicked = {},
onSwipeToReply = {}, onSwipeToReply = {},
onReadReceiptClick = {}, onReadReceiptClick = {},
forceJumpToBottomVisibility = true,
) )
} }
} }

View file

@ -46,6 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.messageFromMeBackground import io.element.android.libraries.designsystem.theme.messageFromMeBackground
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
private val BUBBLE_RADIUS = 12.dp private val BUBBLE_RADIUS = 12.dp
internal val BUBBLE_INCOMING_OFFSET = 16.dp internal val BUBBLE_INCOMING_OFFSET = 16.dp
@ -115,6 +117,7 @@ fun MessageEventBubble(
) { ) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.testTag(TestTags.messageBubble)
.widthIn(min = 80.dp) .widthIn(min = 80.dp)
.clip(bubbleShape) .clip(bubbleShape)
.combinedClickable( .combinedClickable(

View file

@ -49,13 +49,13 @@ import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider
import io.element.android.features.messages.impl.timeline.model.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.aTimelineItemReactions
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
@ -126,9 +126,8 @@ private val ADD_EMOJI_SIZE = 16.dp
@Composable @Composable
private fun TextContent( private fun TextContent(
text: String, text: String,
modifier: Modifier = Modifier,
) = Text( ) = Text(
modifier = modifier modifier = Modifier
.height(REACTION_EMOJI_LINE_HEIGHT.toDp()), .height(REACTION_EMOJI_LINE_HEIGHT.toDp()),
text = text, text = text,
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
@ -138,27 +137,24 @@ private fun TextContent(
@Composable @Composable
private fun IconContent( private fun IconContent(
@DrawableRes resourceId: Int, @DrawableRes resourceId: Int,
modifier: Modifier = Modifier
) = Icon( ) = Icon(
resourceId = resourceId, resourceId = resourceId,
contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction), contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction),
tint = ElementTheme.materialColors.secondary, tint = ElementTheme.materialColors.secondary,
modifier = modifier modifier = Modifier
.size(ADD_EMOJI_SIZE) .size(ADD_EMOJI_SIZE)
) )
@Composable @Composable
private fun ReactionContent( private fun ReactionContent(
reaction: AggregatedReaction, reaction: AggregatedReaction,
modifier: Modifier = Modifier,
) = Row( ) = Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) { ) {
// Check if this is a custom reaction (MSC4027) // Check if this is a custom reaction (MSC4027)
if (reaction.key.startsWith("mxc://")) { if (reaction.key.startsWith("mxc://")) {
AsyncImage( AsyncImage(
modifier = modifier modifier = Modifier
.heightIn(min = REACTION_EMOJI_LINE_HEIGHT.toDp(), max = REACTION_EMOJI_LINE_HEIGHT.toDp()) .heightIn(min = REACTION_EMOJI_LINE_HEIGHT.toDp(), max = REACTION_EMOJI_LINE_HEIGHT.toDp())
.aspectRatio(REACTION_IMAGE_ASPECT_RATIO, false), .aspectRatio(REACTION_IMAGE_ASPECT_RATIO, false),
model = MediaRequestData(MediaSource(reaction.key), MediaRequestData.Kind.Content), model = MediaRequestData(MediaSource(reaction.key), MediaRequestData.Kind.Content),
@ -197,7 +193,7 @@ internal fun MessagesReactionButtonPreview(@PreviewParameter(AggregatedReactionP
@Composable @Composable
internal fun MessagesAddReactionButtonPreview() = ElementPreview { internal fun MessagesAddReactionButtonPreview() = ElementPreview {
MessagesReactionButton( MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction), content = MessagesReactionsButtonContent.Icon(CompoundDrawables.ic_compound_reaction_add),
onClick = {}, onClick = {},
onLongClick = {} onLongClick = {}
) )

View file

@ -25,10 +25,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.utils.CommonDrawables
/** /**
* A swipe indicator that appears when swiping to reply to a message. * A swipe indicator that appears when swiping to reply to a message.
@ -49,7 +49,7 @@ fun RowScope.ReplySwipeIndicator(
alpha = swipeProgress() alpha = swipeProgress()
}, },
contentDescription = null, contentDescription = null,
resourceId = CommonDrawables.ic_reply, imageVector = CompoundIcons.Reply(),
) )
} }

View file

@ -91,7 +91,7 @@ fun TimelineEventTimestampView(
if (hasMessageSendingFailed && tint != null) { if (hasMessageSendingFailed && tint != null) {
Spacer(modifier = Modifier.width(2.dp)) Spacer(modifier = Modifier.width(2.dp))
Icon( Icon(
imageVector = CompoundIcons.Error, imageVector = CompoundIcons.Error(),
contentDescription = stringResource(id = CommonStrings.common_sending_failed), contentDescription = stringResource(id = CommonStrings.common_sending_failed),
tint = tint, tint = tint,
modifier = Modifier.size(15.dp, 18.dp), modifier = Modifier.size(15.dp, 18.dp),

View file

@ -53,6 +53,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@ -92,6 +93,7 @@ import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.EqualWidthColumn
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.swipe.SwipeableActionsState import io.element.android.libraries.designsystem.swipe.SwipeableActionsState
@ -435,7 +437,7 @@ private fun MessageEventBubbleContent(
) { ) {
Icon( Icon(
modifier = Modifier.height(14.dp), modifier = Modifier.height(14.dp),
imageVector = CompoundIcons.Threads, imageVector = CompoundIcons.Threads(),
contentDescription = null, contentDescription = null,
tint = ElementTheme.colors.iconSecondary, tint = ElementTheme.colors.iconSecondary,
) )
@ -648,18 +650,53 @@ private fun ReplyToContent(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Text( ReplyToContentText(metadata)
text = metadata?.text.orEmpty(),
style = ElementTheme.typography.fontBodyMdRegular,
textAlign = TextAlign.Start,
color = ElementTheme.materialColors.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
} }
} }
} }
@Composable
private fun ReplyToContentText(metadata: InReplyToMetadata?) {
val text = when (metadata) {
InReplyToMetadata.Redacted -> stringResource(id = CommonStrings.common_message_removed)
InReplyToMetadata.UnableToDecrypt -> stringResource(id = CommonStrings.common_waiting_for_decryption_key)
is InReplyToMetadata.Text -> metadata.text
is InReplyToMetadata.Thumbnail -> metadata.text
null -> ""
}
val iconResourceId = when (metadata) {
InReplyToMetadata.Redacted -> CompoundDrawables.ic_compound_delete
InReplyToMetadata.UnableToDecrypt -> CompoundDrawables.ic_compound_time
else -> null
}
val fontStyle = when (metadata) {
is InReplyToMetadata.Informative -> FontStyle.Italic
else -> FontStyle.Normal
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
if (iconResourceId != null) {
Icon(
resourceId = iconResourceId,
tint = MaterialTheme.colorScheme.secondary,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
text = text,
style = ElementTheme.typography.fontBodyMdRegular,
fontStyle = fontStyle,
textAlign = TextAlign.Start,
color = MaterialTheme.colorScheme.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun TimelineItemEventRowPreview() = ElementPreview { internal fun TimelineItemEventRowPreview() = ElementPreview {

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
@PreviewsDayNight
@Composable
internal fun TimelineItemEventRowWithReplyInformativePreview(
@PreviewParameter(InReplyToDetailsInformativeProvider::class) inReplyToDetails: InReplyToDetails,
) = ElementPreview {
TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails)
}
class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() {
override val values: Sequence<InReplyToDetails>
get() = sequenceOf(
RedactedContent,
UnableToDecryptContent(UnableToDecryptContent.Data.Unknown),
).map {
aInReplyToDetails(
eventContent = it,
)
}
}

View file

@ -54,6 +54,11 @@ import kotlinx.collections.immutable.persistentMapOf
internal fun TimelineItemEventRowWithReplyPreview( internal fun TimelineItemEventRowWithReplyPreview(
@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails, @PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails,
) = ElementPreview { ) = ElementPreview {
TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails)
}
@Composable
internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InReplyToDetails) {
Column { Column {
sequenceOf(false, true).forEach { sequenceOf(false, true).forEach {
ATimelineItemEventRow( ATimelineItemEventRow(
@ -83,7 +88,7 @@ internal fun TimelineItemEventRowWithReplyPreview(
} }
} }
class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> { open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> {
override val values: Sequence<InReplyToDetails> override val values: Sequence<InReplyToDetails>
get() = sequenceOf( get() = sequenceOf(
aMessageContent( aMessageContent(
@ -156,7 +161,7 @@ class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> {
type = type, type = type,
) )
private fun aInReplyToDetails( protected fun aInReplyToDetails(
eventContent: EventContent, eventContent: EventContent,
) = InReplyToDetails( ) = InReplyToDetails(
eventId = EventId("\$event"), eventId = EventId("\$event"),

View file

@ -26,9 +26,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.R
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.CommonDrawables
/** /**
* A flow layout for reactions that will show a collapse/expand button when the layout wraps over a defined number of rows. * A flow layout for reactions that will show a collapse/expand button when the layout wraps over a defined number of rows.
@ -195,7 +195,7 @@ internal fun TimelineItemReactionsLayoutPreview() = ElementPreview {
}, },
addMoreButton = { addMoreButton = {
MessagesReactionButton( MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction), content = MessagesReactionsButtonContent.Icon(CompoundDrawables.ic_compound_reaction_add),
onClick = {}, onClick = {},
onLongClick = {} onLongClick = {}
) )

View file

@ -31,9 +31,9 @@ import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.CommonDrawables
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Composable @Composable
@ -99,7 +99,7 @@ private fun TimelineItemReactionsView(
addMoreButton = if (userCanSendReaction) { addMoreButton = if (userCanSendReaction) {
{ {
MessagesReactionButton( MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction), content = MessagesReactionsButtonContent.Icon(CompoundDrawables.ic_compound_reaction_add),
onClick = onMoreReactionsClick, onClick = onMoreReactionsClick,
onLongClick = {} onLongClick = {}
) )

View file

@ -21,9 +21,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -36,7 +36,7 @@ fun TimelineItemEncryptedView(
TimelineItemInformativeView( TimelineItemInformativeView(
text = stringResource(id = CommonStrings.common_waiting_for_decryption_key), text = stringResource(id = CommonStrings.common_waiting_for_decryption_key),
iconDescription = stringResource(id = CommonStrings.dialog_title_warning), iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
iconResourceId = CommonDrawables.ic_waiting_to_decrypt, iconResourceId = CompoundDrawables.ic_compound_time,
onContentLayoutChanged = onContentLayoutChanged, onContentLayoutChanged = onContentLayoutChanged,
modifier = modifier modifier = modifier
) )

View file

@ -37,11 +37,11 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentProvider
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
@Composable @Composable
fun TimelineItemFileView( fun TimelineItemFileView(
@ -62,7 +62,7 @@ fun TimelineItemFileView(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Icon( Icon(
resourceId = CommonDrawables.ic_attachment, resourceId = CompoundDrawables.ic_compound_attachment,
contentDescription = null, contentDescription = null,
tint = ElementTheme.materialColors.primary, tint = ElementTheme.materialColors.primary,
modifier = Modifier modifier = Modifier

View file

@ -77,7 +77,7 @@ internal fun TimelineItemInformativeViewPreview() = ElementPreview {
TimelineItemInformativeView( TimelineItemInformativeView(
text = "Info", text = "Info",
iconDescription = "", iconDescription = "",
iconResourceId = CompoundDrawables.ic_delete, iconResourceId = CompoundDrawables.ic_compound_delete,
onContentLayoutChanged = {}, onContentLayoutChanged = {},
) )
} }

View file

@ -35,7 +35,7 @@ fun TimelineItemRedactedView(
TimelineItemInformativeView( TimelineItemInformativeView(
text = stringResource(id = CommonStrings.common_message_removed), text = stringResource(id = CommonStrings.common_message_removed),
iconDescription = stringResource(id = CommonStrings.common_message_removed), iconDescription = stringResource(id = CommonStrings.common_message_removed),
iconResourceId = CompoundDrawables.ic_delete, iconResourceId = CompoundDrawables.ic_compound_delete,
onContentLayoutChanged = onContentLayoutChanged, onContentLayoutChanged = onContentLayoutChanged,
modifier = modifier modifier = modifier
) )

View file

@ -35,7 +35,7 @@ fun TimelineItemUnknownView(
TimelineItemInformativeView( TimelineItemInformativeView(
text = stringResource(id = CommonStrings.common_unsupported_event), text = stringResource(id = CommonStrings.common_unsupported_event),
iconDescription = stringResource(id = CommonStrings.dialog_title_warning), iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
iconResourceId = CompoundDrawables.ic_info_solid, iconResourceId = CompoundDrawables.ic_compound_info_solid,
onContentLayoutChanged = onContentLayoutChanged, onContentLayoutChanged = onContentLayoutChanged,
modifier = modifier modifier = modifier
) )

View file

@ -34,6 +34,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -44,6 +45,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider
@ -58,7 +60,6 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.designsystem.theme.components.Icon 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.IconButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -126,8 +127,8 @@ private fun PlayButton(
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,
) { ) {
Icon( ControlIcon(
resourceId = CommonDrawables.ic_play, imageVector = CompoundIcons.PlaySolid(),
contentDescription = stringResource(id = CommonStrings.a11y_play), contentDescription = stringResource(id = CommonStrings.a11y_play),
) )
} }
@ -140,8 +141,8 @@ private fun PauseButton(
CustomIconButton( CustomIconButton(
onClick = onClick, onClick = onClick,
) { ) {
Icon( ControlIcon(
resourceId = CommonDrawables.ic_pause, imageVector = CompoundIcons.PauseSolid(),
contentDescription = stringResource(id = CommonStrings.a11y_pause), contentDescription = stringResource(id = CommonStrings.a11y_pause),
) )
} }
@ -154,13 +155,25 @@ private fun RetryButton(
CustomIconButton( CustomIconButton(
onClick = onClick, onClick = onClick,
) { ) {
Icon( ControlIcon(
resourceId = CommonDrawables.ic_retry, imageVector = CompoundIcons.Restart(),
contentDescription = stringResource(id = CommonStrings.action_retry), contentDescription = stringResource(id = CommonStrings.action_retry),
) )
} }
} }
@Composable
private fun ControlIcon(
imageVector: ImageVector,
contentDescription: String?,
) {
Icon(
modifier = Modifier.padding(vertical = 10.dp),
imageVector = imageVector,
contentDescription = contentDescription,
)
}
/** /**
* Progress button is shown when the voice message is being downloaded. * Progress button is shown when the voice message is being downloaded.
* *
@ -190,8 +203,8 @@ private fun ProgressButton(
strokeWidth = 2.dp, strokeWidth = 2.dp,
) )
} else { } else {
Icon( ControlIcon(
resourceId = CommonDrawables.ic_pause, imageVector = CompoundIcons.PauseSolid(),
contentDescription = stringResource(id = CommonStrings.a11y_pause), contentDescription = stringResource(id = CommonStrings.a11y_pause),
) )
} }

View file

@ -89,7 +89,7 @@ fun GroupHeaderView(
) )
Icon( Icon(
modifier = Modifier.rotate(rotation), modifier = Modifier.rotate(rotation),
imageVector = CompoundIcons.ChevronRight, imageVector = CompoundIcons.ChevronRight(),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.secondary tint = MaterialTheme.colorScheme.secondary
) )

View file

@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -106,7 +107,6 @@ fun ReactionSummaryView(
@Composable @Composable
private fun SheetContent( private fun SheetContent(
summary: ReactionSummaryState.Summary, summary: ReactionSummaryState.Summary,
modifier: Modifier = Modifier,
) { ) {
val animationScope = rememberCoroutineScope() val animationScope = rememberCoroutineScope()
var selectedReactionKey: String by rememberSaveable { mutableStateOf(summary.selectedKey) } var selectedReactionKey: String by rememberSaveable { mutableStateOf(summary.selectedKey) }
@ -127,9 +127,8 @@ private fun SheetContent(
} }
Column( Column(
modifier = modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.fillMaxHeight()
) { ) {
LazyRow( LazyRow(
state = reactionListState, state = reactionListState,
@ -172,7 +171,6 @@ private fun AggregatedReactionButton(
reaction: AggregatedReaction, reaction: AggregatedReaction,
isHighlighted: Boolean, isHighlighted: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
val buttonColor = if (isHighlighted) { val buttonColor = if (isHighlighted) {
ElementTheme.colors.bgActionPrimaryRest ElementTheme.colors.bgActionPrimaryRest
@ -188,7 +186,7 @@ private fun AggregatedReactionButton(
val roundedCornerShape = RoundedCornerShape(corner = CornerSize(percent = 50)) val roundedCornerShape = RoundedCornerShape(corner = CornerSize(percent = 50))
Surface( Surface(
modifier = modifier modifier = Modifier
.background(buttonColor, roundedCornerShape) .background(buttonColor, roundedCornerShape)
.clip(roundedCornerShape) .clip(roundedCornerShape)
.clickable(onClick = onClick) .clickable(onClick = onClick)
@ -238,10 +236,9 @@ private fun SenderRow(
name: String, name: String,
userId: String, userId: String,
sentTime: String, sentTime: String,
modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 56.dp) .heightIn(min = 56.dp)
.padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp), .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp),

View file

@ -41,6 +41,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import io.element.android.appconfig.TimelineConfig import io.element.android.appconfig.TimelineConfig
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -49,8 +50,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -68,6 +70,7 @@ fun TimelineItemReadReceiptView(
ReadReceiptsAvatars( ReadReceiptsAvatars(
receipts = state.receipts, receipts = state.receipts,
modifier = Modifier modifier = Modifier
.testTag(TestTags.messageReadReceipts)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.clickable { .clickable {
onReadReceiptsClicked() onReadReceiptsClicked()
@ -82,7 +85,7 @@ fun TimelineItemReadReceiptView(
ReadReceiptsRow(modifier) { ReadReceiptsRow(modifier) {
Icon( Icon(
modifier = Modifier.padding(2.dp), modifier = Modifier.padding(2.dp),
resourceId = CommonDrawables.ic_sending, imageVector = CompoundIcons.Circle(),
contentDescription = stringResource(id = CommonStrings.common_sending), contentDescription = stringResource(id = CommonStrings.common_sending),
tint = ElementTheme.colors.iconSecondary tint = ElementTheme.colors.iconSecondary
) )
@ -98,7 +101,7 @@ fun TimelineItemReadReceiptView(
ReadReceiptsRow(modifier = modifier) { ReadReceiptsRow(modifier = modifier) {
Icon( Icon(
modifier = Modifier.padding(2.dp), modifier = Modifier.padding(2.dp),
resourceId = CommonDrawables.ic_sent, imageVector = CompoundIcons.CheckCircle(),
contentDescription = stringResource(id = CommonStrings.common_sent), contentDescription = stringResource(id = CommonStrings.common_sent),
tint = ElementTheme.colors.iconSecondary tint = ElementTheme.colors.iconSecondary
) )

View file

@ -22,10 +22,15 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
class RetrySendMenuStateProvider : PreviewParameterProvider<RetrySendMenuState> { class RetrySendMenuStateProvider : PreviewParameterProvider<RetrySendMenuState> {
override val values: Sequence<RetrySendMenuState> = sequenceOf( override val values: Sequence<RetrySendMenuState> = sequenceOf(
aRetrySendMenuState(event = null), aRetrySendMenuState(),
aRetrySendMenuState(event = aTimelineItemEvent()), aRetrySendMenuState(event = aTimelineItemEvent()),
) )
} }
fun aRetrySendMenuState(event: TimelineItem.Event? = aTimelineItemEvent()) = fun aRetrySendMenuState(
RetrySendMenuState(selectedEvent = event, eventSink = {}) event: TimelineItem.Event? = null,
eventSink: (RetrySendMenuEvents) -> Unit = {},
) = RetrySendMenuState(
selectedEvent = event,
eventSink = eventSink,
)

View file

@ -56,7 +56,7 @@ fun TimelineEncryptedHistoryBannerView(
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.InfoSolid, imageVector = CompoundIcons.InfoSolid(),
contentDescription = null, contentDescription = null,
tint = ElementTheme.colors.iconInfoPrimary tint = ElementTheme.colors.iconInfoPrimary
) )

View file

@ -125,11 +125,10 @@ fun EventDebugInfoView(
private fun CollapsibleSection( private fun CollapsibleSection(
title: String, title: String,
text: String, text: String,
modifier: Modifier = Modifier,
initiallyExpanded: Boolean = false, initiallyExpanded: Boolean = false,
) { ) {
var isExpanded by remember { mutableStateOf(initiallyExpanded) } var isExpanded by remember { mutableStateOf(initiallyExpanded) }
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Row( Row(
modifier = Modifier modifier = Modifier
.clickable { isExpanded = !isExpanded } .clickable { isExpanded = !isExpanded }
@ -140,7 +139,7 @@ private fun CollapsibleSection(
Text(title, modifier = Modifier.weight(1f)) Text(title, modifier = Modifier.weight(1f))
Icon( Icon(
modifier = Modifier.rotate(if (isExpanded) 180f else 0f), modifier = Modifier.rotate(if (isExpanded) 180f else 0f),
imageVector = CompoundIcons.ChevronDown, imageVector = CompoundIcons.ChevronDown(),
contentDescription = null contentDescription = null
) )
} }

View file

@ -21,12 +21,20 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
@ -35,17 +43,20 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Immutable @Immutable
internal sealed interface InReplyToMetadata { internal sealed interface InReplyToMetadata {
val text: String?
data class Thumbnail( data class Thumbnail(
val attachmentThumbnailInfo: AttachmentThumbnailInfo val attachmentThumbnailInfo: AttachmentThumbnailInfo
) : InReplyToMetadata { ) : InReplyToMetadata {
override val text: String? = attachmentThumbnailInfo.textContent val text: String = attachmentThumbnailInfo.textContent.orEmpty()
} }
data class Text( data class Text(
override val text: String val text: String
) : InReplyToMetadata ) : InReplyToMetadata
sealed interface Informative : InReplyToMetadata
data object Redacted : Informative
data object UnableToDecrypt : Informative
} }
/** /**
@ -103,7 +114,8 @@ internal fun InReplyToDetails.metadata(): InReplyToMetadata? = when (eventConten
AttachmentThumbnailInfo( AttachmentThumbnailInfo(
thumbnailSource = MediaSource(eventContent.url), thumbnailSource = MediaSource(eventContent.url),
textContent = eventContent.body, textContent = eventContent.body,
type = AttachmentThumbnailType.Image type = AttachmentThumbnailType.Image,
blurHash = eventContent.info.blurhash,
) )
) )
is PollContent -> InReplyToMetadata.Thumbnail( is PollContent -> InReplyToMetadata.Thumbnail(
@ -112,5 +124,13 @@ internal fun InReplyToDetails.metadata(): InReplyToMetadata? = when (eventConten
type = AttachmentThumbnailType.Poll, type = AttachmentThumbnailType.Poll,
) )
) )
else -> null is RedactedContent -> InReplyToMetadata.Redacted
is UnableToDecryptContent -> InReplyToMetadata.UnableToDecrypt
is FailedToParseMessageLikeContent,
is FailedToParseStateContent,
is ProfileChangeContent,
is RoomMembershipContent,
is StateContent,
UnknownContent,
null -> null
} }

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.typing
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.messages.impl.MessagesView
import io.element.android.features.messages.impl.aMessagesState
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@PreviewsDayNight
@Composable
internal fun MessagesViewWithTypingPreview(
@PreviewParameter(TypingNotificationStateForMessagesProvider::class) typingState: TypingNotificationState
) = ElementPreview {
MessagesView(
state = aMessagesState().copy(typingNotificationState = typingState),
onBackPressed = {},
onRoomDetailsClicked = {},
onEventClicked = { false },
onPreviewAttachments = {},
onUserDataClicked = {},
onSendLocationClicked = {},
onCreatePollClicked = {},
onJoinCallClicked = {},
)
}

View file

@ -0,0 +1,107 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.typing
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.roomMembers
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
class TypingNotificationPresenter @Inject constructor(
private val room: MatrixRoom,
private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter<TypingNotificationState> {
@Composable
override fun present(): TypingNotificationState {
val typingMembersState = remember { mutableStateOf(emptyList<RoomMember>()) }
val renderTypingNotifications by sessionPreferencesStore.isRenderTypingNotificationsEnabled().collectAsState(initial = true)
LaunchedEffect(renderTypingNotifications) {
if (renderTypingNotifications) {
observeRoomTypingMembers(typingMembersState)
} else {
typingMembersState.value = emptyList()
}
}
// This will keep the space reserved for the typing notifications after the first one is displayed
var reserveSpace by remember { mutableStateOf(false) }
LaunchedEffect(renderTypingNotifications, typingMembersState.value) {
if (renderTypingNotifications && typingMembersState.value.isNotEmpty()) {
reserveSpace = true
}
}
return TypingNotificationState(
renderTypingNotifications = renderTypingNotifications,
typingMembers = typingMembersState.value.toImmutableList(),
reserveSpace = reserveSpace,
)
}
private fun CoroutineScope.observeRoomTypingMembers(typingMembersState: MutableState<List<RoomMember>>) {
combine(room.roomTypingMembersFlow, room.membersStateFlow) { typingMembers, membersState ->
typingMembers
.map { userId ->
membersState.roomMembers()
?.firstOrNull { roomMember -> roomMember.userId == userId }
?: createDefaultRoomMemberForTyping(userId)
}
}
.distinctUntilChanged()
.onEach { members ->
typingMembersState.value = members
}
.launchIn(this)
}
}
/**
* Create a default [RoomMember] for typing events.
* In this case, only the userId will be used for rendering, other fields are not used, but keep them
* as close as possible to the actual data.
*/
private fun createDefaultRoomMemberForTyping(userId: UserId): RoomMember {
return RoomMember(
userId = userId,
displayName = null,
avatarUrl = null,
membership = RoomMembershipState.JOIN,
isNameAmbiguous = false,
powerLevel = 0,
normalizedPowerLevel = 0,
isIgnored = false,
)
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.typing
import io.element.android.libraries.matrix.api.room.RoomMember
import kotlinx.collections.immutable.ImmutableList
/**
* State for the typing notification view.
*/
data class TypingNotificationState(
/** Whether to render the typing notifications based on the user's preferences. */
val renderTypingNotifications: Boolean,
/** The room members currently typing. */
val typingMembers: ImmutableList<RoomMember>,
/** Whether to reserve space for the typing notifications at the bottom of the timeline. */
val reserveSpace: Boolean,
)

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.typing
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
class TypingNotificationStateForMessagesProvider : PreviewParameterProvider<TypingNotificationState> {
override val values: Sequence<TypingNotificationState>
get() = sequenceOf(
aTypingNotificationState(
typingMembers = listOf(
aTypingRoomMember(displayName = "Alice"),
aTypingRoomMember(displayName = "Bob"),
),
),
aTypingNotificationState(
typingMembers = listOf(aTypingRoomMember()),
reserveSpace = true
),
aTypingNotificationState(reserveSpace = true),
)
}

View file

@ -0,0 +1,102 @@
/*
* 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.typing
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import kotlinx.collections.immutable.toImmutableList
class TypingNotificationStateProvider : PreviewParameterProvider<TypingNotificationState> {
override val values: Sequence<TypingNotificationState>
get() = sequenceOf(
aTypingNotificationState(),
aTypingNotificationState(
typingMembers = listOf(
aTypingRoomMember(),
),
),
aTypingNotificationState(
typingMembers = listOf(
aTypingRoomMember(displayName = "Alice"),
),
),
aTypingNotificationState(
typingMembers = listOf(
aTypingRoomMember(displayName = "Alice", isNameAmbiguous = true),
),
),
aTypingNotificationState(
typingMembers = listOf(
aTypingRoomMember(displayName = "Alice"),
aTypingRoomMember(displayName = "Bob"),
),
),
aTypingNotificationState(
typingMembers = listOf(
aTypingRoomMember(displayName = "Alice"),
aTypingRoomMember(displayName = "Bob"),
aTypingRoomMember(displayName = "Charlie"),
),
),
aTypingNotificationState(
typingMembers = listOf(
aTypingRoomMember(displayName = "Alice"),
aTypingRoomMember(displayName = "Bob"),
aTypingRoomMember(displayName = "Charlie"),
aTypingRoomMember(displayName = "Dan"),
aTypingRoomMember(displayName = "Eve"),
),
),
aTypingNotificationState(
typingMembers = listOf(
aTypingRoomMember(displayName = "Alice with a very long display name which means that it will be truncated"),
),
),
aTypingNotificationState(
typingMembers = emptyList(),
reserveSpace = true,
),
)
}
internal fun aTypingNotificationState(
typingMembers: List<RoomMember> = emptyList(),
reserveSpace: Boolean = false,
) = TypingNotificationState(
renderTypingNotifications = true,
typingMembers = typingMembers.toImmutableList(),
reserveSpace = reserveSpace,
)
internal fun aTypingRoomMember(
userId: UserId = UserId("@alice:example.com"),
displayName: String? = null,
isNameAmbiguous: Boolean = false,
): RoomMember {
return RoomMember(
userId = userId,
displayName = displayName,
avatarUrl = null,
membership = RoomMembershipState.JOIN,
isNameAmbiguous = isNameAmbiguous,
powerLevel = 0,
normalizedPowerLevel = 0,
isIgnored = false,
)
}

Some files were not shown because too many files have changed in this diff Show more