Merge branch 'develop' into feature/fga/room_list_filters

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

View file

@ -56,7 +56,7 @@ jobs:
path: |
app/build/outputs/apk/gplay/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
# Do not fail the whole build if Diawi upload fails
continue-on-error: true

View file

@ -8,6 +8,7 @@ appId: ${APP_ID}
# Back from timeline
- back
- assertVisible: "MyR"
- hideKeyboard
# Back from search
- back
- 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)
========================================

View file

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

View file

@ -84,10 +84,8 @@ fun LoadingRoomNodeView(
@Composable
private fun LoadingRoomTopBar(
onBackClicked: () -> Unit,
modifier: Modifier = Modifier
) {
TopAppBar(
modifier = modifier,
navigationIcon = {
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.FakeRageshakeDataStore
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.AppErrorStateService
import io.element.android.services.apperror.impl.DefaultAppErrorStateService
@ -83,6 +84,7 @@ class RootPresenterTest {
val rageshake = FakeRageShake()
val screenshotHolder = FakeScreenshotHolder()
val crashDetectionPresenter = DefaultCrashDetectionPresenter(
buildMeta = aBuildMeta(),
crashDataStore = crashDataStore
)
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.TextButton
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 kotlinx.collections.immutable.persistentListOf
@ -67,7 +66,6 @@ fun AnalyticsOptInView(
onClickTerms: () -> Unit,
modifier: Modifier = Modifier,
) {
LogCompositions(tag = "Analytics", msg = "Root")
val eventSink = state.eventSink
fun onTermsAccepted() {
@ -101,10 +99,8 @@ private const val LINK_TAG = "link"
private fun AnalyticsOptInHeader(
state: AnalyticsOptInState,
onClickTerms: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
IconTitleSubtitleMolecule(
@ -141,24 +137,22 @@ private fun AnalyticsOptInHeader(
}
@Composable
private fun CheckIcon(modifier: Modifier = Modifier) {
private fun CheckIcon() {
Icon(
modifier = modifier
modifier = Modifier
.size(20.dp)
.background(color = MaterialTheme.colorScheme.background, shape = CircleShape)
.padding(2.dp),
imageVector = CompoundIcons.Check,
imageVector = CompoundIcons.Check(),
contentDescription = null,
tint = ElementTheme.colors.textActionAccent,
)
}
@Composable
private fun AnalyticsOptInContent(
modifier: Modifier = Modifier,
) {
private fun AnalyticsOptInContent() {
Box(
modifier = modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
contentAlignment = BiasAlignment(
horizontalBias = 0f,
verticalBias = -0.4f
@ -190,11 +184,8 @@ private fun AnalyticsOptInContent(
private fun AnalyticsOptInFooter(
onTermsAccepted: () -> Unit,
onTermsDeclined: () -> Unit,
modifier: Modifier = Modifier,
) {
ButtonColumnMolecule(
modifier = modifier,
) {
ButtonColumnMolecule {
Button(
text = stringResource(id = CommonStrings.action_ok),
onClick = onTermsAccepted,

View file

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

View file

@ -88,10 +88,8 @@ private fun AddPeopleViewTopBar(
hasSelectedUsers: Boolean,
onBackPressed: () -> Unit,
onNextPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
modifier = modifier,
title = {
Text(
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 androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -38,8 +37,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
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.res.stringResource
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.AsyncActionViewDefaults
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.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
@ -173,10 +171,8 @@ private fun ConfigureRoomToolbar(
isNextActionEnabled: Boolean,
onBackPressed: () -> Unit,
onNextPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
modifier = modifier,
title = {
Text(
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
@Composable
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {

View file

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

View file

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

View file

@ -33,7 +33,6 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.impl.migration.MigrationScreenNode
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.FtueStep
@ -74,9 +73,6 @@ class FtueFlowNode @AssistedInject constructor(
@Parcelize
data object Placeholder : NavTarget
@Parcelize
data object MigrationScreen : NavTarget
@Parcelize
data object WelcomeScreen : NavTarget
@ -114,14 +110,6 @@ class FtueFlowNode @AssistedInject constructor(
NavTarget.Placeholder -> {
createNode<PlaceholderNode>(buildContext)
}
NavTarget.MigrationScreen -> {
val callback = object : MigrationScreenNode.Callback {
override fun onMigrationFinished() {
lifecycleScope.launch { moveToNextStep() }
}
}
createNode<MigrationScreenNode>(buildContext, listOf(callback))
}
NavTarget.WelcomeScreen -> {
val callback = object : WelcomeNode.Callback {
override fun onContinueClicked() {
@ -158,9 +146,6 @@ class FtueFlowNode @AssistedInject constructor(
private fun moveToNextStep() {
when (ftueState.getNextStep()) {
FtueStep.MigrationScreen -> {
backstack.newRoot(NavTarget.MigrationScreen)
}
FtueStep.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)) },
footer = { NotificationsOptInFooter(state) },
) {
NotificationsOptInContent(modifier = Modifier.fillMaxWidth())
NotificationsOptInContent()
}
}
@ -79,7 +79,7 @@ private fun NotificationsOptInHeader(
modifier = modifier,
title = stringResource(R.string.screen_notification_optin_title),
subTitle = stringResource(R.string.screen_notification_optin_subtitle),
iconImageVector = CompoundIcons.NotificationsSolid,
iconImageVector = CompoundIcons.NotificationsSolid(),
)
}
@ -104,10 +104,8 @@ private fun NotificationsOptInFooter(state: NotificationsOptInState) {
}
@Composable
private fun NotificationsOptInContent(
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
private fun NotificationsOptInContent() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(
verticalArrangement = Arrangement.spacedBy(
16.dp,
@ -144,10 +142,8 @@ private fun NotificationRow(
avatarColorsId: String,
firstRowPercent: Float,
secondRowPercent: Float,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
color = ElementTheme.colors.bgCanvasDisabled,
shape = RoundedCornerShape(14.dp),
shadowElevation = 2.dp,

View file

@ -21,11 +21,9 @@ import android.os.Build
import androidx.annotation.VisibleForTesting
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.ftue.impl.migration.MigrationScreenStore
import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState
import io.element.android.features.lockscreen.api.LockScreenService
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.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ -43,17 +41,14 @@ class DefaultFtueState @Inject constructor(
coroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
private val welcomeScreenState: WelcomeScreenState,
private val migrationScreenStore: MigrationScreenStore,
private val permissionStateProvider: PermissionStateProvider,
private val lockScreenService: LockScreenService,
private val matrixClient: MatrixClient,
) : FtueState {
override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete())
override suspend fun reset() {
welcomeScreenState.reset()
analyticsService.reset()
migrationScreenStore.reset()
if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS)
}
@ -67,12 +62,7 @@ class DefaultFtueState @Inject constructor(
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
when (currentStep) {
null -> if (shouldDisplayMigrationScreen()) {
FtueStep.MigrationScreen
} else {
getNextStep(FtueStep.MigrationScreen)
}
FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) {
null -> if (shouldDisplayWelcomeScreen()) {
FtueStep.WelcomeScreen
} else {
getNextStep(FtueStep.WelcomeScreen)
@ -97,7 +87,6 @@ class DefaultFtueState @Inject constructor(
private fun isAnyStepIncomplete(): Boolean {
return listOf(
{ shouldDisplayMigrationScreen() },
{ shouldDisplayWelcomeScreen() },
{ shouldAskNotificationPermissions() },
{ needsAnalyticsOptIn() },
@ -105,10 +94,6 @@ class DefaultFtueState @Inject constructor(
).any { it() }
}
private fun shouldDisplayMigrationScreen(): Boolean {
return migrationScreenStore.isMigrationScreenNeeded(matrixClient.sessionId)
}
private fun needsAnalyticsOptIn(): Boolean {
// We need this function to not be suspend, so we need to load the value through runBlocking
return runBlocking { analyticsService.didAskUserConsent().first().not() }
@ -147,7 +132,6 @@ class DefaultFtueState @Inject constructor(
}
sealed interface FtueStep {
data object MigrationScreen : FtueStep
data object WelcomeScreen : FtueStep
data object NotificationsOptIn : 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.theme.components.Button
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.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@ -102,11 +101,11 @@ fun WelcomeView(
private fun listItems() = persistentListOf(
InfoListItem(
message = stringResource(R.string.screen_welcome_bullet_2),
iconId = CommonDrawables.ic_lock_outline,
iconVector = CompoundIcons.Lock(),
),
InfoListItem(
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"?>
<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_title">"Дазвольце апавяшчэнні і ніколі не прапускайце іх"</string>
<string name="screen_welcome_bullet_1">"Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_2">"在這次的更新,您無法查看聊天室內被加密的歷史訊息。"</string>
<string name="screen_welcome_bullet_3">"我們很樂意聽取您的意見,請到設定頁面告訴我們您的想法。"</string>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

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

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

View file

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

View file

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

View file

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

View file

@ -57,13 +57,12 @@ fun SetupBiometricView(
}
@Composable
private fun SetupBiometricHeader(modifier: Modifier = Modifier) {
private fun SetupBiometricHeader() {
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
IconTitleSubtitleMolecule(
iconImageVector = Icons.Default.Fingerprint,
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),
modifier = modifier
)
}
@ -71,11 +70,8 @@ private fun SetupBiometricHeader(modifier: Modifier = Modifier) {
private fun SetupBiometricFooter(
onAllowClicked: () -> Unit,
onSkipClicked: () -> Unit,
modifier: Modifier = Modifier
) {
ButtonColumnMolecule(
modifier = modifier,
) {
ButtonColumnMolecule {
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
Button(
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(
isValidationStep: Boolean,
appName: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
IconTitleSubtitleMolecule(
@ -107,7 +105,6 @@ private fun SetupPinHeader(
@Composable
private fun SetupPinContent(
state: SetupPinState,
modifier: Modifier = Modifier,
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
@ -119,14 +116,13 @@ private fun SetupPinContent(
onValueChange = { entry ->
state.eventSink(SetupPinEvents.OnPinEntryChanged(entry, state.isConfirmationStep))
},
modifier = modifier
modifier = Modifier
.focusRequester(focusRequester)
.padding(top = 36.dp)
.fillMaxWidth()
)
if (state.setupPinFailure != null) {
ErrorDialog(
modifier = modifier,
title = state.setupPinFailure.title(),
content = state.setupPinFailure.content(),
onDismiss = {

View file

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

View file

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

View file

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

View file

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

View file

@ -103,7 +103,7 @@ fun SearchAccountProviderView(
item {
IconTitleSubtitleMolecule(
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),
subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle),
)
@ -139,7 +139,7 @@ fun SearchAccountProviderView(
eventSink(SearchAccountProviderEvents.UserInput(""))
}) {
Icon(
imageVector = CompoundIcons.Close,
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_clear)
)
}

View file

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

View file

@ -24,12 +24,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
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.ui.LogoutActionDialog
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.Text
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.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.RecoveryState
@ -64,7 +62,7 @@ fun LogoutView(
onBackClicked = onBackClicked,
title = title(state),
subTitle = subtitle(state),
iconVector = ImageVector.vectorResource(CommonDrawables.ic_key),
iconVector = CompoundIcons.KeySolid(),
modifier = modifier,
content = { Content(state) },
buttons = {
@ -86,7 +84,7 @@ fun LogoutView(
onForceLogoutClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = true))
},
onDismissError = {
onDismissDialog = {
eventSink(LogoutEvents.CloseDialogs)
},
onSuccessLogout = {

View file

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

View file

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

View file

@ -62,6 +62,7 @@ dependencies {
implementation(projects.libraries.voicerecorder.api)
implementation(projects.libraries.mediaplayer.api)
implementation(projects.libraries.uiUtils)
implementation(projects.libraries.testtags)
implementation(projects.features.networkmonitor.api)
implementation(projects.services.analytics.api)
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.TimelineItemVideoContent
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.voicemessages.composer.VoiceMessageComposerPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
@ -97,6 +98,7 @@ class MessagesPresenter @AssistedInject constructor(
private val composerPresenter: MessageComposerPresenter,
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter,
timelinePresenterFactory: TimelinePresenter.Factory,
private val typingNotificationPresenter: TypingNotificationPresenter,
private val actionListPresenter: ActionListPresenter,
private val customReactionPresenter: CustomReactionPresenter,
private val reactionSummaryPresenter: ReactionSummaryPresenter,
@ -129,6 +131,7 @@ class MessagesPresenter @AssistedInject constructor(
val composerState = composerPresenter.present()
val voiceMessageComposerState = voiceMessageComposerPresenter.present()
val timelineState = timelinePresenter.present()
val typingNotificationState = typingNotificationPresenter.present()
val actionListState = actionListPresenter.present()
val customReactionState = customReactionPresenter.present()
val reactionSummaryState = reactionSummaryPresenter.present()
@ -155,6 +158,14 @@ class MessagesPresenter @AssistedInject constructor(
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) {
withContext(dispatchers.io) {
canJoinCall = room.canUserJoinCall(room.sessionId).getOrDefault(false)
@ -225,6 +236,7 @@ class MessagesPresenter @AssistedInject constructor(
composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState,
timelineState = timelineState,
typingNotificationState = typingNotificationState,
actionListState = actionListState,
customReactionState = customReactionState,
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.receipt.bottomsheet.ReadReceiptBottomSheetState
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.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@ -42,6 +43,7 @@ data class MessagesState(
val composerState: MessageComposerState,
val voiceMessageComposerState: VoiceMessageComposerState,
val timelineState: TimelineState,
val typingNotificationState: TypingNotificationState,
val actionListState: ActionListState,
val customReactionState: CustomReactionState,
val reactionSummaryState: ReactionSummaryState,

View file

@ -17,16 +17,26 @@
package io.element.android.features.messages.impl
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.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.timeline.TimelineState
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.components.customreaction.CustomReactionEvents
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.receipt.bottomsheet.ReadReceiptBottomSheetEvents
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.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.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.aVoiceMessagePreviewState
import io.element.android.libraries.architecture.AsyncData
@ -42,87 +52,120 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
override val values: Sequence<MessagesState>
get() = sequenceOf(
aMessagesState(),
aMessagesState().copy(hasNetworkConnection = false),
aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)),
aMessagesState().copy(userHasPermissionToSendMessage = false),
aMessagesState().copy(showReinvitePrompt = true),
aMessagesState().copy(
aMessagesState(hasNetworkConnection = false),
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
aMessagesState(userHasPermissionToSendMessage = false),
aMessagesState(showReinvitePrompt = true),
aMessagesState(
roomName = AsyncData.Uninitialized,
roomAvatar = AsyncData.Uninitialized,
),
aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)),
aMessagesState().copy(
aMessagesState(composerState = aMessageComposerState(showTextFormatting = true)),
aMessagesState(
enableVoiceMessages = true,
voiceMessageComposerState = aVoiceMessageComposerState(showPermissionRationaleDialog = true),
),
aMessagesState().copy(
composerState = aMessageComposerState().copy(
aMessagesState(
composerState = aMessageComposerState(
attachmentsState = AttachmentsState.Sending.Processing(persistentListOf())
),
),
aMessagesState().copy(
composerState = aMessageComposerState().copy(
aMessagesState(
composerState = aMessageComposerState(
attachmentsState = AttachmentsState.Sending.Uploading(0.33f)
),
),
aMessagesState().copy(
aMessagesState(
callState = RoomCallState.ONGOING,
),
aMessagesState().copy(
aMessagesState(
enableVoiceMessages = true,
voiceMessageComposerState = aVoiceMessageComposerState(
voiceMessageState = aVoiceMessagePreviewState(),
showSendFailureDialog = true
),
),
aMessagesState().copy(
aMessagesState(
callState = RoomCallState.DISABLED,
),
)
}
fun aMessagesState() = MessagesState(
roomId = RoomId("!id:domain"),
roomName = AsyncData.Success("Room name"),
roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
userHasPermissionToSendMessage = true,
userHasPermissionToRedactOwn = false,
userHasPermissionToRedactOther = false,
userHasPermissionToSendReaction = true,
composerState = aMessageComposerState().copy(
fun aMessagesState(
roomName: AsyncData<String> = AsyncData.Success("Room name"),
roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
userHasPermissionToSendMessage: Boolean = true,
userHasPermissionToRedactOwn: Boolean = false,
userHasPermissionToRedactOther: Boolean = false,
userHasPermissionToSendReaction: Boolean = true,
composerState: MessageComposerState = aMessageComposerState(
richTextEditorState = RichTextEditorState("Hello", initialFocus = true),
isFullScreen = false,
mode = MessageComposerMode.Normal,
),
voiceMessageComposerState = aVoiceMessageComposerState(),
timelineState = aTimelineState().copy(
voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
timelineState: TimelineState = aTimelineState(
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
),
retrySendMenuState = RetrySendMenuState(
selectedEvent = null,
eventSink = {},
),
readReceiptBottomSheetState = ReadReceiptBottomSheetState(
selectedEvent = null,
eventSink = {},
),
actionListState = anActionListState(),
customReactionState = CustomReactionState(
target = CustomReactionState.Target.None,
eventSink = {},
selectedEmoji = persistentSetOf(),
),
reactionSummaryState = ReactionSummaryState(
target = null,
eventSink = {},
),
hasNetworkConnection = true,
retrySendMenuState: RetrySendMenuState = aRetrySendMenuState(),
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
actionListState: ActionListState = anActionListState(),
customReactionState: CustomReactionState = aCustomReactionState(),
reactionSummaryState: ReactionSummaryState = aReactionSummaryState(),
hasNetworkConnection: Boolean = true,
showReinvitePrompt: Boolean = false,
enableVoiceMessages: Boolean = true,
callState: RoomCallState = RoomCallState.ENABLED,
eventSink: (MessagesEvents) -> Unit = {},
) = MessagesState(
roomId = RoomId("!id:domain"),
roomName = roomName,
roomAvatar = roomAvatar,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState,
typingNotificationState = aTypingNotificationState(),
timelineState = timelineState,
retrySendMenuState = retrySendMenuState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
actionListState = actionListState,
customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = null,
inviteProgress = AsyncData.Uninitialized,
showReinvitePrompt = false,
showReinvitePrompt = showReinvitePrompt,
enableTextFormatting = true,
enableVoiceMessages = true,
callState = RoomCallState.ENABLED,
enableVoiceMessages = enableVoiceMessages,
callState = callState,
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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.TopAppBar
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.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
@ -126,9 +124,8 @@ fun MessagesView(
onCreatePollClicked: () -> Unit,
onJoinCallClicked: () -> Unit,
modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false
) {
LogCompositions(tag = "MessagesScreen", msg = "Root")
OnLifecycleEvent { _, 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
val localView = LocalView.current
LogCompositions(tag = "MessagesScreen", msg = "Content")
fun onMessageClicked(event: TimelineItem.Event) {
Timber.v("OnMessageClicked= ${event.id}")
val hideKeyboard = onEventClicked(event)
@ -198,7 +193,12 @@ fun MessagesView(
roomName = state.roomName.dataOrNull(),
roomAvatar = state.roomAvatar.dataOrNull(),
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,
onJoinCallClicked = onJoinCallClicked,
)
@ -229,6 +229,7 @@ fun MessagesView(
onSwipeToReply = { targetEvent ->
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
},
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
)
},
snackbarHost = {
@ -253,7 +254,6 @@ fun MessagesView(
state = state.customReactionState,
onEmojiSelected = { eventId, emoji ->
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
}
)
@ -264,14 +264,6 @@ fun MessagesView(
onUserDataClicked = onUserDataClicked,
)
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
@ -329,6 +321,7 @@ private fun MessagesViewContent(
onTimestampClicked: (TimelineItem.Event) -> Unit,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
forceJumpToBottomVisibility: Boolean,
modifier: Modifier = Modifier,
onSwipeToReply: (TimelineItem.Event) -> Unit,
) {
@ -389,6 +382,7 @@ private fun MessagesViewContent(
modifier = Modifier.padding(paddingValues),
state = state.timelineState,
roomName = state.roomName.dataOrNull(),
typingNotificationState = state.typingNotificationState,
onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked,
onUserDataClicked = onUserDataClicked,
@ -398,6 +392,7 @@ private fun MessagesViewContent(
onMoreReactionsClicked = onMoreReactionsClicked,
onReadReceiptClick = onReadReceiptClick,
onSwipeToReply = onSwipeToReply,
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
)
},
sheetContent = { subcomposing: Boolean ->
@ -417,10 +412,9 @@ private fun MessagesViewContent(
private fun MessagesViewComposerBottomSheetContents(
subcomposing: Boolean,
state: MessagesState,
modifier: Modifier = Modifier,
) {
if (state.userHasPermissionToSendMessage) {
Column(modifier = modifier.fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth()) {
MentionSuggestionsPickerView(
modifier = Modifier
.heightIn(max = 230.dp)
@ -448,7 +442,7 @@ private fun MessagesViewComposerBottomSheetContents(
)
}
} else {
CantSendMessageBanner(modifier = modifier)
CantSendMessageBanner()
}
}
@ -461,10 +455,8 @@ private fun MessagesViewTopBar(
onRoomDetailsClicked: () -> Unit,
onJoinCallClicked: () -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
modifier = modifier,
navigationIcon = {
BackButton(onClick = onBackPressed)
},
@ -489,7 +481,7 @@ private fun MessagesViewTopBar(
} else {
IconButton(onClick = onJoinCallClicked, enabled = callState != RoomCallState.DISABLED) {
Icon(
imageVector = CompoundIcons.VideoCallSolid,
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_start_call),
)
}
@ -502,7 +494,6 @@ private fun MessagesViewTopBar(
@Composable
private fun JoinCallMenuItem(
modifier: Modifier = Modifier,
onJoinCallClicked: () -> Unit,
) {
Material3Button(
@ -512,11 +503,11 @@ private fun JoinCallMenuItem(
containerColor = ElementTheme.colors.iconAccentTertiary
),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
modifier = modifier.heightIn(min = 36.dp),
modifier = Modifier.heightIn(min = 36.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.VideoCallSolid,
imageVector = CompoundIcons.VideoCallSolid(),
contentDescription = null
)
Spacer(Modifier.width(8.dp))
@ -550,11 +541,9 @@ private fun RoomAvatarAndNameRow(
}
@Composable
private fun CantSendMessageBanner(
modifier: Modifier = Modifier,
) {
private fun CantSendMessageBanner() {
Row(
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondary)
.padding(16.dp),
@ -584,5 +573,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onSendLocationClicked = {},
onCreatePollClicked = {},
onJoinCallClicked = {},
forceJumpToBottomVisibility = true,
)
}

View file

@ -122,9 +122,12 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
}
}
fun anActionListState() = ActionListState(
target = ActionListState.Target.None,
eventSink = {}
fun anActionListState(
target: ActionListState.Target = ActionListState.Target.None,
eventSink: (ActionListEvents) -> Unit = {},
) = ActionListState(
target = target,
eventSink = eventSink
)
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.unit.dp
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.timeline.model.TimelineItem
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.Text
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 kotlinx.collections.immutable.ImmutableList
@ -316,7 +316,7 @@ private fun EmojiReactionsRow(
contentAlignment = Alignment.Center
) {
Icon(
resourceId = CommonDrawables.ic_add_reaction,
imageVector = CompoundIcons.ReactionAdd(),
contentDescription = stringResource(id = CommonStrings.a11y_react_with_other_emojis),
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier
@ -337,7 +337,6 @@ private fun EmojiButton(
emoji: String,
isHighlighted: Boolean,
onClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val backgroundColor = if (isHighlighted) {
ElementTheme.colors.bgActionPrimaryRest
@ -350,7 +349,7 @@ private fun EmojiButton(
stringResource(id = CommonStrings.a11y_react_with, emoji)
}
Box(
modifier = modifier
modifier = Modifier
.size(48.dp)
.background(backgroundColor, CircleShape)
.clearAndSetSemantics {

View file

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

View file

@ -118,10 +118,9 @@ private fun AttachmentPreviewContent(
attachment: Attachment,
onSendClicked: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
modifier = Modifier
.fillMaxSize()
.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.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -101,34 +100,33 @@ private fun AttachmentSourcePickerMenu(
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
enableTextFormatting: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
modifier = Modifier
.navigationBarsPadding()
.imePadding()
) {
ListItem(
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)) },
style = ListItemStyle.Primary,
)
ListItem(
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)) },
style = ListItemStyle.Primary,
)
ListItem(
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)) },
style = ListItemStyle.Primary,
)
ListItem(
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)) },
style = ListItemStyle.Primary,
)
@ -138,7 +136,7 @@ private fun AttachmentSourcePickerMenu(
state.eventSink(MessageComposerEvents.PickAttachmentSource.Location)
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)) },
style = ListItemStyle.Primary,
)
@ -149,7 +147,7 @@ private fun AttachmentSourcePickerMenu(
state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll)
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)) },
style = ListItemStyle.Primary,
)
@ -157,7 +155,7 @@ private fun AttachmentSourcePickerMenu(
if (enableTextFormatting) {
ListItem(
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)) },
style = ListItemStyle.Primary,
)

View file

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

View file

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

View file

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

View file

@ -47,7 +47,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
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.event.TimelineItemEventContent
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.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -75,6 +77,7 @@ import kotlinx.coroutines.launch
@Composable
fun TimelineView(
state: TimelineState,
typingNotificationState: TypingNotificationState,
roomName: String?,
onUserDataClicked: (UserId) -> Unit,
onMessageClicked: (TimelineItem.Event) -> Unit,
@ -86,6 +89,7 @@ fun TimelineView(
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false
) {
fun onReachedLoadMore() {
state.eventSink(TimelineEvents.LoadMore)
@ -112,6 +116,9 @@ fun TimelineView(
reverseLayout = true,
contentPadding = PaddingValues(vertical = 8.dp),
) {
item {
TypingNotificationView(state = typingNotificationState)
}
items(
items = state.timelineItems,
contentType = { timelineItem -> timelineItem.contentType() },
@ -157,6 +164,7 @@ fun TimelineView(
TimelineScrollHelper(
isTimelineEmpty = state.timelineItems.isEmpty(),
lazyListState = lazyListState,
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
newEventState = state.newEventState,
onScrollFinishedAt = ::onScrollFinishedAt
)
@ -168,6 +176,7 @@ private fun BoxScope.TimelineScrollHelper(
isTimelineEmpty: Boolean,
lazyListState: LazyListState,
newEventState: NewEventState,
forceJumpToBottomVisibility: Boolean,
onScrollFinishedAt: (Int) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
@ -205,7 +214,7 @@ private fun BoxScope.TimelineScrollHelper(
JumpToBottomButton(
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
isVisible = !canAutoScroll,
isVisible = !canAutoScroll || forceJumpToBottomVisibility,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
@ -221,7 +230,7 @@ private fun JumpToBottomButton(
) {
AnimatedVisibility(
modifier = modifier,
visible = isVisible || LocalInspectionMode.current,
visible = isVisible,
enter = scaleIn(animationSpec = tween(100)),
exit = scaleOut(animationSpec = tween(100)),
) {
@ -237,7 +246,7 @@ private fun JumpToBottomButton(
modifier = Modifier
.size(24.dp)
.rotate(90f),
imageVector = CompoundIcons.ArrowRight,
imageVector = CompoundIcons.ArrowRight(),
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
)
}
@ -256,6 +265,7 @@ internal fun TimelineViewPreview(
TimelineView(
state = aTimelineState(timelineItems),
roomName = null,
typingNotificationState = aTypingNotificationState(),
onMessageClicked = {},
onTimestampClicked = {},
onUserDataClicked = {},
@ -265,6 +275,7 @@ internal fun TimelineViewPreview(
onMoreReactionsClicked = {},
onSwipeToReply = {},
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.messageFromMeBackground
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
internal val BUBBLE_INCOMING_OFFSET = 16.dp
@ -115,6 +117,7 @@ fun MessageEventBubble(
) {
Surface(
modifier = Modifier
.testTag(TestTags.messageBubble)
.widthIn(min = 80.dp)
.clip(bubbleShape)
.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.AggregatedReactionProvider
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.PreviewsDayNight
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.Surface
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.ui.media.MediaRequestData
@ -126,9 +126,8 @@ private val ADD_EMOJI_SIZE = 16.dp
@Composable
private fun TextContent(
text: String,
modifier: Modifier = Modifier,
) = Text(
modifier = modifier
modifier = Modifier
.height(REACTION_EMOJI_LINE_HEIGHT.toDp()),
text = text,
style = ElementTheme.typography.fontBodyMdRegular,
@ -138,27 +137,24 @@ private fun TextContent(
@Composable
private fun IconContent(
@DrawableRes resourceId: Int,
modifier: Modifier = Modifier
) = Icon(
resourceId = resourceId,
contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction),
tint = ElementTheme.materialColors.secondary,
modifier = modifier
modifier = Modifier
.size(ADD_EMOJI_SIZE)
)
@Composable
private fun ReactionContent(
reaction: AggregatedReaction,
modifier: Modifier = Modifier,
) = Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
// Check if this is a custom reaction (MSC4027)
if (reaction.key.startsWith("mxc://")) {
AsyncImage(
modifier = modifier
modifier = Modifier
.heightIn(min = REACTION_EMOJI_LINE_HEIGHT.toDp(), max = REACTION_EMOJI_LINE_HEIGHT.toDp())
.aspectRatio(REACTION_IMAGE_ASPECT_RATIO, false),
model = MediaRequestData(MediaSource(reaction.key), MediaRequestData.Kind.Content),
@ -197,7 +193,7 @@ internal fun MessagesReactionButtonPreview(@PreviewParameter(AggregatedReactionP
@Composable
internal fun MessagesAddReactionButtonPreview() = ElementPreview {
MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction),
content = MessagesReactionsButtonContent.Icon(CompoundDrawables.ic_compound_reaction_add),
onClick = {},
onLongClick = {}
)

View file

@ -25,10 +25,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
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.PreviewsDayNight
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.
@ -49,7 +49,7 @@ fun RowScope.ReplySwipeIndicator(
alpha = swipeProgress()
},
contentDescription = null,
resourceId = CommonDrawables.ic_reply,
imageVector = CompoundIcons.Reply(),
)
}

View file

@ -91,7 +91,7 @@ fun TimelineEventTimestampView(
if (hasMessageSendingFailed && tint != null) {
Spacer(modifier = Modifier.width(2.dp))
Icon(
imageVector = CompoundIcons.Error,
imageVector = CompoundIcons.Error(),
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
tint = tint,
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.ViewConfiguration
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.TextOverflow
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.avatar.Avatar
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.PreviewsDayNight
import io.element.android.libraries.designsystem.swipe.SwipeableActionsState
@ -435,7 +437,7 @@ private fun MessageEventBubbleContent(
) {
Icon(
modifier = Modifier.height(14.dp),
imageVector = CompoundIcons.Threads,
imageVector = CompoundIcons.Threads(),
contentDescription = null,
tint = ElementTheme.colors.iconSecondary,
)
@ -648,18 +650,53 @@ private fun ReplyToContent(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = metadata?.text.orEmpty(),
style = ElementTheme.typography.fontBodyMdRegular,
textAlign = TextAlign.Start,
color = ElementTheme.materialColors.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
ReplyToContentText(metadata)
}
}
}
@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
@Composable
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(
@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails,
) = ElementPreview {
TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails)
}
@Composable
internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InReplyToDetails) {
Column {
sequenceOf(false, true).forEach {
ATimelineItemEventRow(
@ -83,7 +88,7 @@ internal fun TimelineItemEventRowWithReplyPreview(
}
}
class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> {
open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> {
override val values: Sequence<InReplyToDetails>
get() = sequenceOf(
aMessageContent(
@ -156,7 +161,7 @@ class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> {
type = type,
)
private fun aInReplyToDetails(
protected fun aInReplyToDetails(
eventContent: EventContent,
) = InReplyToDetails(
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.dp
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.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.
@ -195,7 +195,7 @@ internal fun TimelineItemReactionsLayoutPreview() = ElementPreview {
},
addMoreButton = {
MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction),
content = MessagesReactionsButtonContent.Icon(CompoundDrawables.ic_compound_reaction_add),
onClick = {},
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.model.AggregatedReaction
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.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.CommonDrawables
import kotlinx.collections.immutable.ImmutableList
@Composable
@ -99,7 +99,7 @@ private fun TimelineItemReactionsView(
addMoreButton = if (userCanSendReaction) {
{
MessagesReactionButton(
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction),
content = MessagesReactionsButtonContent.Icon(CompoundDrawables.ic_compound_reaction_add),
onClick = onMoreReactionsClick,
onLongClick = {}
)

View file

@ -21,9 +21,9 @@ import androidx.compose.ui.Modifier
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.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.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.ui.strings.CommonStrings
@ -36,7 +36,7 @@ fun TimelineItemEncryptedView(
TimelineItemInformativeView(
text = stringResource(id = CommonStrings.common_waiting_for_decryption_key),
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
iconResourceId = CommonDrawables.ic_waiting_to_decrypt,
iconResourceId = CompoundDrawables.ic_compound_time,
onContentLayoutChanged = onContentLayoutChanged,
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.model.event.TimelineItemFileContent
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.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
@Composable
fun TimelineItemFileView(
@ -62,7 +62,7 @@ fun TimelineItemFileView(
contentAlignment = Alignment.Center,
) {
Icon(
resourceId = CommonDrawables.ic_attachment,
resourceId = CompoundDrawables.ic_compound_attachment,
contentDescription = null,
tint = ElementTheme.materialColors.primary,
modifier = Modifier

View file

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

View file

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

View file

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

View file

@ -34,6 +34,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
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.unit.dp
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.model.event.TimelineItemVoiceContent
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.IconButton
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 kotlinx.coroutines.delay
@ -126,8 +127,8 @@ private fun PlayButton(
onClick = onClick,
enabled = enabled,
) {
Icon(
resourceId = CommonDrawables.ic_play,
ControlIcon(
imageVector = CompoundIcons.PlaySolid(),
contentDescription = stringResource(id = CommonStrings.a11y_play),
)
}
@ -140,8 +141,8 @@ private fun PauseButton(
CustomIconButton(
onClick = onClick,
) {
Icon(
resourceId = CommonDrawables.ic_pause,
ControlIcon(
imageVector = CompoundIcons.PauseSolid(),
contentDescription = stringResource(id = CommonStrings.a11y_pause),
)
}
@ -154,13 +155,25 @@ private fun RetryButton(
CustomIconButton(
onClick = onClick,
) {
Icon(
resourceId = CommonDrawables.ic_retry,
ControlIcon(
imageVector = CompoundIcons.Restart(),
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.
*
@ -190,8 +203,8 @@ private fun ProgressButton(
strokeWidth = 2.dp,
)
} else {
Icon(
resourceId = CommonDrawables.ic_pause,
ControlIcon(
imageVector = CompoundIcons.PauseSolid(),
contentDescription = stringResource(id = CommonStrings.a11y_pause),
)
}

View file

@ -89,7 +89,7 @@ fun GroupHeaderView(
)
Icon(
modifier = Modifier.rotate(rotation),
imageVector = CompoundIcons.ChevronRight,
imageVector = CompoundIcons.ChevronRight(),
contentDescription = null,
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
@ -106,7 +107,6 @@ fun ReactionSummaryView(
@Composable
private fun SheetContent(
summary: ReactionSummaryState.Summary,
modifier: Modifier = Modifier,
) {
val animationScope = rememberCoroutineScope()
var selectedReactionKey: String by rememberSaveable { mutableStateOf(summary.selectedKey) }
@ -127,9 +127,8 @@ private fun SheetContent(
}
Column(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight()
modifier = Modifier
.fillMaxSize()
) {
LazyRow(
state = reactionListState,
@ -172,7 +171,6 @@ private fun AggregatedReactionButton(
reaction: AggregatedReaction,
isHighlighted: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val buttonColor = if (isHighlighted) {
ElementTheme.colors.bgActionPrimaryRest
@ -188,7 +186,7 @@ private fun AggregatedReactionButton(
val roundedCornerShape = RoundedCornerShape(corner = CornerSize(percent = 50))
Surface(
modifier = modifier
modifier = Modifier
.background(buttonColor, roundedCornerShape)
.clip(roundedCornerShape)
.clickable(onClick = onClick)
@ -238,10 +236,9 @@ private fun SenderRow(
name: String,
userId: String,
sentTime: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 56.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 io.element.android.appconfig.TimelineConfig
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.libraries.designsystem.components.avatar.Avatar
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.theme.components.Icon
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.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
@ -68,6 +70,7 @@ fun TimelineItemReadReceiptView(
ReadReceiptsAvatars(
receipts = state.receipts,
modifier = Modifier
.testTag(TestTags.messageReadReceipts)
.clip(RoundedCornerShape(4.dp))
.clickable {
onReadReceiptsClicked()
@ -82,7 +85,7 @@ fun TimelineItemReadReceiptView(
ReadReceiptsRow(modifier) {
Icon(
modifier = Modifier.padding(2.dp),
resourceId = CommonDrawables.ic_sending,
imageVector = CompoundIcons.Circle(),
contentDescription = stringResource(id = CommonStrings.common_sending),
tint = ElementTheme.colors.iconSecondary
)
@ -98,7 +101,7 @@ fun TimelineItemReadReceiptView(
ReadReceiptsRow(modifier = modifier) {
Icon(
modifier = Modifier.padding(2.dp),
resourceId = CommonDrawables.ic_sent,
imageVector = CompoundIcons.CheckCircle(),
contentDescription = stringResource(id = CommonStrings.common_sent),
tint = ElementTheme.colors.iconSecondary
)

View file

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

View file

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

View file

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

View file

@ -21,12 +21,20 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource
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.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.ImageMessageType
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.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.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.VoiceMessageType
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
@ -35,17 +43,20 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Immutable
internal sealed interface InReplyToMetadata {
val text: String?
data class Thumbnail(
val attachmentThumbnailInfo: AttachmentThumbnailInfo
) : InReplyToMetadata {
override val text: String? = attachmentThumbnailInfo.textContent
val text: String = attachmentThumbnailInfo.textContent.orEmpty()
}
data class Text(
override val text: String
val text: String
) : 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(
thumbnailSource = MediaSource(eventContent.url),
textContent = eventContent.body,
type = AttachmentThumbnailType.Image
type = AttachmentThumbnailType.Image,
blurHash = eventContent.info.blurhash,
)
)
is PollContent -> InReplyToMetadata.Thumbnail(
@ -112,5 +124,13 @@ internal fun InReplyToDetails.metadata(): InReplyToMetadata? = when (eventConten
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