Merge branch 'develop' into feature/fga/room_list_filters
This commit is contained in:
commit
ffec57e455
1489 changed files with 5726 additions and 4385 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 s’agit d’une opération ponctuelle, merci d’attendre 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ fun AccountProviderView(
|
|||
} else {
|
||||
RoundedIconAtom(
|
||||
size = RoundedIconAtomSize.Medium,
|
||||
imageVector = CompoundIcons.Search,
|
||||
imageVector = CompoundIcons.Search(),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
|
|||
onForceLogoutClicked = {
|
||||
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true))
|
||||
},
|
||||
onDismissError = {
|
||||
onDismissDialog = {
|
||||
eventSink(DirectLogoutEvents.CloseDialogs)
|
||||
},
|
||||
onSuccessLogout = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ internal fun TimelineItemInformativeViewPreview() = ElementPreview {
|
|||
TimelineItemInformativeView(
|
||||
text = "Info",
|
||||
iconDescription = "",
|
||||
iconResourceId = CompoundDrawables.ic_delete,
|
||||
iconResourceId = CompoundDrawables.ic_compound_delete,
|
||||
onContentLayoutChanged = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ fun GroupHeaderView(
|
|||
)
|
||||
Icon(
|
||||
modifier = Modifier.rotate(rotation),
|
||||
imageVector = CompoundIcons.ChevronRight,
|
||||
imageVector = CompoundIcons.ChevronRight(),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ fun TimelineEncryptedHistoryBannerView(
|
|||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = CompoundIcons.InfoSolid,
|
||||
imageVector = CompoundIcons.InfoSolid(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconInfoPrimary
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun TypingNotificationView(
|
||||
state: TypingNotificationState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val displayNotifications = state.typingMembers.isNotEmpty() && state.renderTypingNotifications
|
||||
|
||||
@Suppress("ModifierNaming")
|
||||
@Composable fun TypingText(text: AnnotatedString, textModifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = textModifier,
|
||||
text = text,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
// Display the typing notification space when either a typing notification needs to be displayed or a previous one already was
|
||||
AnimatedVisibility(
|
||||
modifier = modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
visible = displayNotifications || state.reserveSpace,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
val typingNotificationText = computeTypingNotificationText(state.typingMembers)
|
||||
Box(contentAlignment = Alignment.BottomStart) {
|
||||
// Reserve the space for the typing notification by adding an invisible text
|
||||
TypingText(
|
||||
text = typingNotificationText,
|
||||
textModifier = Modifier
|
||||
.alpha(0f)
|
||||
// Remove the semantics of the text to avoid screen readers to read it
|
||||
.clearAndSetSemantics { }
|
||||
)
|
||||
|
||||
// Display the actual notification
|
||||
AnimatedVisibility(
|
||||
visible = displayNotifications,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
TypingText(text = typingNotificationText, textModifier = Modifier.padding(horizontal = 24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun computeTypingNotificationText(typingMembers: ImmutableList<RoomMember>): AnnotatedString {
|
||||
// Remember the last value to avoid empty typing messages while animating
|
||||
var result by remember { mutableStateOf(AnnotatedString("")) }
|
||||
if (typingMembers.isNotEmpty()) {
|
||||
val names = when (typingMembers.size) {
|
||||
0 -> "" // Cannot happen
|
||||
1 -> typingMembers[0].disambiguatedDisplayName
|
||||
2 -> stringResource(
|
||||
id = R.string.screen_room_typing_two_members,
|
||||
typingMembers[0].disambiguatedDisplayName,
|
||||
typingMembers[1].disambiguatedDisplayName,
|
||||
)
|
||||
else -> pluralStringResource(
|
||||
id = R.plurals.screen_room_typing_many_members,
|
||||
count = typingMembers.size - 2,
|
||||
typingMembers[0].disambiguatedDisplayName,
|
||||
typingMembers[1].disambiguatedDisplayName,
|
||||
typingMembers.size - 2,
|
||||
)
|
||||
}
|
||||
// Get the translated string with a fake pattern
|
||||
val tmpString = pluralStringResource(
|
||||
id = R.plurals.screen_room_typing_notification,
|
||||
count = typingMembers.size,
|
||||
"<>",
|
||||
)
|
||||
// Split the string in 3 parts
|
||||
val parts = tmpString.split("<>")
|
||||
// And rebuild the string with the names
|
||||
result = buildAnnotatedString {
|
||||
append(parts[0])
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(names)
|
||||
}
|
||||
append(parts[1])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TypingNotificationViewPreview(
|
||||
@PreviewParameter(TypingNotificationStateProvider::class) state: TypingNotificationState,
|
||||
) = ElementPreview {
|
||||
TypingNotificationView(
|
||||
modifier = if (state.reserveSpace) Modifier.border(1.dp, Color.Blue) else Modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
|
@ -12,6 +12,14 @@
|
|||
<item quantity="one">"%1$d Raumänderung"</item>
|
||||
<item quantity="other">"%1$d Raumänderungen"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s und %3$d weitere Person"</item>
|
||||
<item quantity="other">"%1$s, %2$s und %3$d weitere Person"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s schreibt…"</item>
|
||||
<item quantity="other">"%1$s schreiben…"</item>
|
||||
</plurals>
|
||||
<string name="report_content_explanation">"Diese Meldung wird an den Administrator deines Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen."</string>
|
||||
<string name="report_content_hint">"Grund für die Meldung dieses Inhalts"</string>
|
||||
<string name="room_timeline_beginning_of_room">"Dies ist der Anfang von %1$s."</string>
|
||||
|
|
@ -52,6 +60,7 @@
|
|||
<string name="screen_room_retry_send_menu_title">"Deine Nachricht konnte nicht gesendet werden"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Emoji hinzufügen"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Weniger anzeigen"</string>
|
||||
<string name="screen_room_typing_two_members">"%1$s und %2$s"</string>
|
||||
<string name="screen_room_voice_message_tooltip">"Zum Aufnehmen gedrückt halten"</string>
|
||||
<string name="screen_room_mentions_at_room_title">"Alle"</string>
|
||||
<string name="screen_report_content_block_user">"Benutzer blockieren"</string>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@
|
|||
<item quantity="one">"%1$d szobaváltozás"</item>
|
||||
<item quantity="other">"%1$d szobaváltozás"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s és %3$d további felhasználó"</item>
|
||||
<item quantity="other">"%1$s, %2$s és %3$d további felhasználó"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s éppen ír…"</item>
|
||||
<item quantity="other">"%1$s éppen ír…"</item>
|
||||
</plurals>
|
||||
<string name="report_content_explanation">"Ez az üzenet jelentve lesz a Matrix-kiszolgáló rendszergazdájának. Nem fogja tudni elolvasni a titkosított üzeneteket."</string>
|
||||
<string name="report_content_hint">"A tartalom jelentésének oka"</string>
|
||||
<string name="room_timeline_beginning_of_room">"Ez a(z) %1$s kezdete."</string>
|
||||
|
|
@ -52,6 +60,7 @@
|
|||
<string name="screen_room_retry_send_menu_title">"Az üzenet elküldése sikertelen"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Emodzsi hozzáadása"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Kevesebb megjelenítése"</string>
|
||||
<string name="screen_room_typing_two_members">"%1$s és %2$s"</string>
|
||||
<string name="screen_room_voice_message_tooltip">"Tartsa a rögzítéshez"</string>
|
||||
<string name="screen_room_mentions_at_room_title">"Mindenki"</string>
|
||||
<string name="screen_report_content_block_user">"Felhasználó letiltása"</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,16 @@
|
|||
<item quantity="few">"%1$d zmeny miestnosti"</item>
|
||||
<item quantity="other">"%1$d zmien miestnosti"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s a %3$d ďalší"</item>
|
||||
<item quantity="few">"%1$s, %2$s a %3$d ďalší"</item>
|
||||
<item quantity="other">"%1$s, %2$s a %3$d ďalší"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s píše"</item>
|
||||
<item quantity="few">"%1$s píšu"</item>
|
||||
<item quantity="other">"%1$s píšu"</item>
|
||||
</plurals>
|
||||
<string name="report_content_explanation">"Táto správa bude nahlásená správcovi vášho domovského servera. Nebude môcť prečítať žiadne šifrované správy."</string>
|
||||
<string name="report_content_hint">"Dôvod nahlásenia tohto obsahu"</string>
|
||||
<string name="room_timeline_beginning_of_room">"Toto je začiatok %1$s."</string>
|
||||
|
|
@ -53,6 +63,7 @@
|
|||
<string name="screen_room_retry_send_menu_title">"Vašu správu sa nepodarilo odoslať"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Pridať emoji"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Zobraziť menej"</string>
|
||||
<string name="screen_room_typing_two_members">"%1$s a %2$s"</string>
|
||||
<string name="screen_room_voice_message_tooltip">"Podržaním nahrajte"</string>
|
||||
<string name="screen_room_mentions_at_room_title">"Všetci"</string>
|
||||
<string name="screen_report_content_block_user">"Zablokovať používateľa"</string>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
|
||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
||||
|
|
@ -96,7 +97,9 @@ import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
|||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
|
@ -129,6 +132,21 @@ class MessagesPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - check that the room's unread flag is removed`() = runTest {
|
||||
val room = FakeMatrixRoom()
|
||||
assertThat(room.markAsReadCalls).isEmpty()
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
runCurrent()
|
||||
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
|
|
@ -713,6 +731,10 @@ class MessagesPresenterTest {
|
|||
}
|
||||
}
|
||||
val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore)
|
||||
val typingNotificationPresenter = TypingNotificationPresenter(
|
||||
room = matrixRoom,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||
|
|
@ -722,6 +744,7 @@ class MessagesPresenterTest {
|
|||
composerPresenter = messageComposerPresenter,
|
||||
voiceMessageComposerPresenter = voiceMessageComposerPresenter,
|
||||
timelinePresenterFactory = timelinePresenterFactory,
|
||||
typingNotificationPresenter = typingNotificationPresenter,
|
||||
actionListPresenter = actionListPresenter,
|
||||
customReactionPresenter = customReactionPresenter,
|
||||
reactionSummaryPresenter = reactionSummaryPresenter,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,503 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.longClick
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onFirst
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeRight
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.emojibasebindings.Emoji
|
||||
import io.element.android.emojibasebindings.EmojibaseCategory
|
||||
import io.element.android.emojibasebindings.EmojibaseStore
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
|
||||
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.receipt.aReadReceiptData
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
|
||||
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.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParamAndResult
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessagesViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
onBackPressed = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on room name invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
onRoomDetailsClicked = callback,
|
||||
)
|
||||
rule.onNodeWithText(state.roomName.dataOrNull().orEmpty()).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on join call invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
onJoinCallClicked = callback,
|
||||
)
|
||||
val joinCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_call)
|
||||
rule.onNodeWithContentDescription(joinCallContentDescription).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on an Event invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first()
|
||||
val callback = EnsureCalledOnceWithParam(
|
||||
expectedParam = timelineItem,
|
||||
result = true,
|
||||
)
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
onEventClicked = callback,
|
||||
)
|
||||
// Cannot perform click on "Text", it's not detected. Use tag instead
|
||||
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick()
|
||||
callback.assertSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on an Event timestamp in error emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
|
||||
val state = aMessagesState(
|
||||
retrySendMenuState = aRetrySendMenuState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems[1] as TimelineItem.Event
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
)
|
||||
rule.onAllNodesWithText(timelineItem.sentTime)[1].performClick()
|
||||
eventsRecorder.assertSingle(RetrySendMenuEvents.EventSelected(timelineItem))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long clicking on an Event emits the expected Event userHasPermissionToSendMessage`() {
|
||||
`long clicking on an Event emits the expected Event`(userHasPermissionToSendMessage = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long clicking on an Event emits the expected Event userHasPermissionToRedactOwn`() {
|
||||
`long clicking on an Event emits the expected Event`(userHasPermissionToRedactOwn = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long clicking on an Event emits the expected Event userHasPermissionToRedactOther`() {
|
||||
`long clicking on an Event emits the expected Event`(userHasPermissionToRedactOther = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long clicking on an Event emits the expected Event userHasPermissionToSendReaction`() {
|
||||
`long clicking on an Event emits the expected Event`(userHasPermissionToSendReaction = true)
|
||||
}
|
||||
|
||||
private fun `long clicking on an Event emits the expected Event`(
|
||||
userHasPermissionToSendMessage: Boolean = false,
|
||||
userHasPermissionToRedactOwn: Boolean = false,
|
||||
userHasPermissionToRedactOther: Boolean = false,
|
||||
userHasPermissionToSendReaction: Boolean = false,
|
||||
) {
|
||||
val eventsRecorder = EventsRecorder<ActionListEvents>()
|
||||
val state = aMessagesState(
|
||||
actionListState = anActionListState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
)
|
||||
// Cannot perform click on "Text", it's not detected. Use tag instead
|
||||
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() }
|
||||
eventsRecorder.assertSingle(
|
||||
ActionListEvents.ComputeForMessage(
|
||||
event = timelineItem,
|
||||
canRedactOwn = state.userHasPermissionToRedactOwn,
|
||||
canRedactOther = state.userHasPermissionToRedactOther,
|
||||
canSendMessage = state.userHasPermissionToSendMessage,
|
||||
canSendReaction = state.userHasPermissionToSendReaction,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a read receipt list emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<ReadReceiptBottomSheetEvents>()
|
||||
val state = aMessagesState(
|
||||
timelineState = aTimelineState(
|
||||
renderReadReceipts = true,
|
||||
timelineItems = persistentListOf(
|
||||
aTimelineItemEvent(
|
||||
readReceiptState = aTimelineItemReadReceipts(
|
||||
receipts = listOf(
|
||||
aReadReceiptData(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
readReceiptBottomSheetState = aReadReceiptBottomSheetState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
)
|
||||
rule.onNodeWithTag(TestTags.messageReadReceipts.value).performClick()
|
||||
eventsRecorder.assertSingle(ReadReceiptBottomSheetEvents.EventSelected(timelineItem))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `swiping on an Event emits the expected Event`() {
|
||||
swipeTest(userHasPermissionToSendMessage = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `swiping on an Event emits no Event if user does not have permission to send message`() {
|
||||
swipeTest(userHasPermissionToSendMessage = false)
|
||||
}
|
||||
|
||||
private fun swipeTest(userHasPermissionToSendMessage: Boolean) {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
||||
val state = aMessagesState(
|
||||
timelineState = aTimelineState(
|
||||
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
|
||||
timelineRoomInfo = aTimelineRoomInfo(
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage
|
||||
),
|
||||
),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
)
|
||||
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { swipeRight(endX = 200f) }
|
||||
if (userHasPermissionToSendMessage) {
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
eventsRecorder.assertSingle(MessagesEvents.HandleAction(TimelineItemAction.Reply, timelineItem))
|
||||
} else {
|
||||
eventsRecorder.assertEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on send location invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
composerState = aMessageComposerState(
|
||||
showAttachmentSourcePicker = true
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
onSendLocationClicked = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_attachment_source_location)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on create poll invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
composerState = aMessageComposerState(
|
||||
showAttachmentSourcePicker = true
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
onCreatePollClicked = callback,
|
||||
)
|
||||
// Then click on the poll action
|
||||
rule.clickOn(R.string.screen_room_attachment_source_poll)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on the sender of an Event invoke expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first()
|
||||
ensureCalledOnceWithParam(
|
||||
param = (timelineItem as TimelineItem.Event).senderId
|
||||
) { callback ->
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
onUserDataClicked = callback,
|
||||
)
|
||||
val senderName = (timelineItem as? TimelineItem.Event)?.senderDisplayName.orEmpty()
|
||||
rule.onNodeWithText(senderName).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting a action on a message emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
val stateWithMessageAction = state.copy(
|
||||
actionListState = anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = timelineItem,
|
||||
displayEmojiReactions = true,
|
||||
actions = persistentListOf(TimelineItemAction.Edit),
|
||||
)
|
||||
),
|
||||
)
|
||||
rule.setMessagesView(
|
||||
state = stateWithMessageAction,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_edit)
|
||||
// Give time for the close animation to complete
|
||||
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
|
||||
eventsRecorder.assertSingle(MessagesEvents.HandleAction(TimelineItemAction.Edit, timelineItem))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a reaction emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
)
|
||||
rule.onAllNodesWithText("👍️").onFirst().performClick()
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.eventId!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long clicking on a reaction emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<ReactionSummaryEvents>()
|
||||
val state = aMessagesState(
|
||||
reactionSummaryState = aReactionSummaryState(
|
||||
target = null,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
)
|
||||
rule.onAllNodesWithText("👍️").onFirst().performTouchInput { longClick() }
|
||||
eventsRecorder.assertSingle(ReactionSummaryEvents.ShowReactionSummary(timelineItem.eventId!!, timelineItem.reactionsState.reactions, "👍️"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on more reaction emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<CustomReactionEvents>()
|
||||
val state = aMessagesState(
|
||||
customReactionState = aCustomReactionState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
rule.setMessagesView(
|
||||
state = state,
|
||||
)
|
||||
val moreReactionContentDescription = rule.activity.getString(R.string.screen_room_timeline_add_reaction)
|
||||
rule.onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick()
|
||||
eventsRecorder.assertSingle(CustomReactionEvents.ShowCustomReactionSheet(timelineItem))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on more reaction from action list emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<CustomReactionEvents>()
|
||||
val state = aMessagesState()
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
val stateWithActionListState = state.copy(
|
||||
actionListState = anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = timelineItem,
|
||||
displayEmojiReactions = true,
|
||||
actions = persistentListOf(TimelineItemAction.Edit),
|
||||
),
|
||||
),
|
||||
customReactionState = aCustomReactionState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.setMessagesView(
|
||||
state = stateWithActionListState,
|
||||
)
|
||||
val moreReactionContentDescription = rule.activity.getString(CommonStrings.a11y_react_with_other_emojis)
|
||||
rule.onNodeWithContentDescription(moreReactionContentDescription).performClick()
|
||||
// Give time for the close animation to complete
|
||||
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
|
||||
eventsRecorder.assertSingle(CustomReactionEvents.ShowCustomReactionSheet(timelineItem))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a custom emoji emits the expected Events`() {
|
||||
val aUnicode = "🙈"
|
||||
val customReactionStateEventsRecorder = EventsRecorder<CustomReactionEvents>()
|
||||
val eventsRecorder = EventsRecorder<MessagesEvents>()
|
||||
val state = aMessagesState(
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
|
||||
val stateWithCustomReactionState = state.copy(
|
||||
customReactionState = aCustomReactionState(
|
||||
target = CustomReactionState.Target.Success(
|
||||
event = timelineItem,
|
||||
emojibaseStore = EmojibaseStore(
|
||||
categories = mapOf(
|
||||
EmojibaseCategory.People to listOf(
|
||||
Emoji(
|
||||
hexcode = "",
|
||||
label = "",
|
||||
tags = emptyList(),
|
||||
shortcodes = emptyList(),
|
||||
unicode = aUnicode,
|
||||
skins = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
eventSink = customReactionStateEventsRecorder
|
||||
),
|
||||
)
|
||||
rule.setMessagesView(
|
||||
state = stateWithCustomReactionState,
|
||||
)
|
||||
rule.onNodeWithText(aUnicode, useUnmergedTree = true).performClick()
|
||||
// Give time for the close animation to complete
|
||||
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
|
||||
customReactionStateEventsRecorder.assertSingle(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.eventId!!))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessagesView(
|
||||
state: MessagesState,
|
||||
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||
onRoomDetailsClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onEventClicked: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(),
|
||||
onUserDataClicked: (UserId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onSendLocationClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onCreatePollClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinCallClicked: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
// Cannot use the RichTextEditor, so simulate a LocalInspectionMode
|
||||
CompositionLocalProvider(LocalInspectionMode provides true) {
|
||||
MessagesView(
|
||||
state = state,
|
||||
onBackPressed = onBackPressed,
|
||||
onRoomDetailsClicked = onRoomDetailsClicked,
|
||||
onEventClicked = onEventClicked,
|
||||
onUserDataClicked = onUserDataClicked,
|
||||
onPreviewAttachments = onPreviewAttachments,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
onJoinCallClicked = onJoinCallClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSende
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
|
|
@ -60,8 +61,11 @@ import io.element.android.tests.testutils.awaitWithLatch
|
|||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
|
@ -69,6 +73,7 @@ import java.util.Date
|
|||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||
private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||
|
||||
class TimelinePresenterTest {
|
||||
@get:Rule
|
||||
|
|
@ -125,11 +130,45 @@ class TimelinePresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) {
|
||||
val timeline = FakeMatrixTimeline(
|
||||
initialTimelineItems = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
)
|
||||
)
|
||||
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
|
||||
val room = FakeMatrixRoom(matrixTimeline = timeline)
|
||||
val presenter = createTimelinePresenter(
|
||||
timeline = timeline,
|
||||
room = room,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||
runCurrent()
|
||||
assertThat(room.markAsReadCalls).isNotEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
|
||||
val timeline = FakeMatrixTimeline(
|
||||
initialTimelineItems = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createTimelinePresenter(timeline)
|
||||
|
|
@ -140,7 +179,7 @@ class TimelinePresenterTest {
|
|||
val initialState = awaitFirstItem()
|
||||
awaitWithLatch { latch ->
|
||||
timeline.sendReadReceiptLatch = latch
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||
}
|
||||
assertThat(timeline.sentReadReceipts).isNotEmpty()
|
||||
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ)
|
||||
|
|
@ -149,10 +188,17 @@ class TimelinePresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - on scroll finished send a private read receipt if an event is before the index and public read receipts are disabled`() = runTest {
|
||||
fun `present - on scroll finished send a private read receipt if an event is at an index other than 0 and public read receipts are disabled`() = runTest {
|
||||
val timeline = FakeMatrixTimeline(
|
||||
initialTimelineItems = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
|
||||
|
|
@ -168,6 +214,7 @@ class TimelinePresenterTest {
|
|||
awaitWithLatch { latch ->
|
||||
timeline.sendReadReceiptLatch = latch
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||
}
|
||||
assertThat(timeline.sentReadReceipts).isNotEmpty()
|
||||
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE)
|
||||
|
|
@ -176,10 +223,17 @@ class TimelinePresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - on scroll finished will not send read receipt if no event is before the index`() = runTest {
|
||||
fun `present - on scroll finished will not send read receipt the first visible event is the same as before`() = runTest {
|
||||
val timeline = FakeMatrixTimeline(
|
||||
initialTimelineItems = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = FAKE_UNIQUE_ID_2,
|
||||
event = anEventTimelineItem(
|
||||
eventId = AN_EVENT_ID_2,
|
||||
content = aMessageContent("Test message")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createTimelinePresenter(timeline)
|
||||
|
|
@ -191,8 +245,9 @@ class TimelinePresenterTest {
|
|||
awaitWithLatch { latch ->
|
||||
timeline.sendReadReceiptLatch = latch
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||
}
|
||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||
assertThat(timeline.sentReadReceipts).hasSize(1)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
|
@ -201,6 +256,7 @@ class TimelinePresenterTest {
|
|||
fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest {
|
||||
val timeline = FakeMatrixTimeline(
|
||||
initialTimelineItems = listOf(
|
||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
||||
)
|
||||
)
|
||||
|
|
@ -212,7 +268,7 @@ class TimelinePresenterTest {
|
|||
val initialState = awaitFirstItem()
|
||||
awaitWithLatch { latch ->
|
||||
timeline.sendReadReceiptLatch = latch
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||
}
|
||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
|
|
@ -41,6 +42,7 @@ class TimelineViewTest {
|
|||
hasMoreToLoadBackwards = true,
|
||||
)
|
||||
),
|
||||
typingNotificationState = aTypingNotificationState(),
|
||||
roomName = null,
|
||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||
onMessageClicked = EnsureNeverCalledWithParam(),
|
||||
|
|
@ -67,6 +69,7 @@ class TimelineViewTest {
|
|||
hasMoreToLoadBackwards = false,
|
||||
)
|
||||
),
|
||||
typingNotificationState = aTypingNotificationState(),
|
||||
roomName = null,
|
||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||
onMessageClicked = EnsureNeverCalledWithParam(),
|
||||
|
|
|
|||
|
|
@ -31,13 +31,23 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
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.OtherState
|
||||
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.test.AN_EVENT_ID
|
||||
|
|
@ -45,11 +55,13 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
|||
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class InReplyToMetadataKtTest {
|
||||
|
|
@ -72,7 +84,7 @@ class InReplyToMetadataKtTest {
|
|||
messageType = ImageMessageType(
|
||||
body = "body",
|
||||
source = aMediaSource(),
|
||||
info = null,
|
||||
info = anImageInfo(),
|
||||
)
|
||||
)
|
||||
).metadata()
|
||||
|
|
@ -84,7 +96,33 @@ class InReplyToMetadataKtTest {
|
|||
thumbnailSource = aMediaSource(),
|
||||
textContent = "body",
|
||||
type = AttachmentThumbnailType.Image,
|
||||
blurHash = null,
|
||||
blurHash = A_BLUR_HASH,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a sticker message content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = StickerContent(
|
||||
body = "body",
|
||||
info = anImageInfo(),
|
||||
url = "url"
|
||||
)
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isEqualTo(
|
||||
InReplyToMetadata.Thumbnail(
|
||||
attachmentThumbnailInfo = AttachmentThumbnailInfo(
|
||||
thumbnailSource = aMediaSource(url = "url"),
|
||||
textContent = "body",
|
||||
type = AttachmentThumbnailType.Image,
|
||||
blurHash = A_BLUR_HASH,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -100,16 +138,7 @@ class InReplyToMetadataKtTest {
|
|||
messageType = VideoMessageType(
|
||||
body = "body",
|
||||
source = aMediaSource(),
|
||||
info = VideoInfo(
|
||||
duration = null,
|
||||
height = null,
|
||||
width = null,
|
||||
mimetype = null,
|
||||
size = null,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = aMediaSource(),
|
||||
blurhash = null
|
||||
),
|
||||
info = aVideoInfo(),
|
||||
)
|
||||
)
|
||||
).metadata()
|
||||
|
|
@ -121,7 +150,7 @@ class InReplyToMetadataKtTest {
|
|||
thumbnailSource = aMediaSource(),
|
||||
textContent = "body",
|
||||
type = AttachmentThumbnailType.Video,
|
||||
blurHash = null,
|
||||
blurHash = A_BLUR_HASH,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -277,11 +306,115 @@ class InReplyToMetadataKtTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `any other content`() = runTest {
|
||||
fun `redacted content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = RedactedContent
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isEqualTo(InReplyToMetadata.Redacted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unable to decrypt content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = UnableToDecryptContent(UnableToDecryptContent.Data.Unknown)
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isEqualTo(InReplyToMetadata.UnableToDecrypt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed to parse message content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = FailedToParseMessageLikeContent("", "")
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed to parse state content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = FailedToParseStateContent("", "", "")
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `profile change content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = ProfileChangeContent("", "", "", "")
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `room membership content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = RoomMembershipContent(A_USER_ID, null)
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = StateContent("", OtherState.RoomJoinRules)
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = UnknownContent
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null content`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
anInReplyToDetails(
|
||||
eventContent = null
|
||||
).metadata()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
assertThat(it).isNull()
|
||||
|
|
@ -306,6 +439,31 @@ fun anInReplyToDetails(
|
|||
textContent = textContent,
|
||||
)
|
||||
|
||||
fun aVideoInfo(): VideoInfo {
|
||||
return VideoInfo(
|
||||
duration = 1.minutes,
|
||||
height = 100,
|
||||
width = 100,
|
||||
mimetype = "video/mp4",
|
||||
size = 1000,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = aMediaSource(),
|
||||
blurhash = A_BLUR_HASH,
|
||||
)
|
||||
}
|
||||
|
||||
fun anImageInfo(): ImageInfo {
|
||||
return ImageInfo(
|
||||
height = 100,
|
||||
width = 100,
|
||||
mimetype = "image/jpeg",
|
||||
size = 1000,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = aMediaSource(),
|
||||
blurhash = A_BLUR_HASH,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun testEnv(content: @Composable () -> Any?): Any? {
|
||||
var result: Any? = null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* 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 app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.Event
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||
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.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class TypingNotificationPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.renderTypingNotifications).isTrue()
|
||||
assertThat(initialState.typingMembers).isEmpty()
|
||||
assertThat(initialState.reserveSpace).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - typing notification disabled`() = runTest {
|
||||
val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2)
|
||||
val room = FakeMatrixRoom()
|
||||
val sessionPreferencesStore = InMemorySessionPreferencesStore(
|
||||
isRenderTypingNotificationsEnabled = false
|
||||
)
|
||||
val presenter = createPresenter(
|
||||
matrixRoom = room,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.renderTypingNotifications).isFalse()
|
||||
assertThat(initialState.typingMembers).isEmpty()
|
||||
room.givenRoomTypingMembers(listOf(A_USER_ID_2))
|
||||
expectNoEvents()
|
||||
// Preferences changes
|
||||
sessionPreferencesStore.setRenderTypingNotifications(true)
|
||||
skipItems(1)
|
||||
val oneMemberTypingState = awaitItem()
|
||||
assertThat(oneMemberTypingState.renderTypingNotifications).isTrue()
|
||||
assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1)
|
||||
assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember)
|
||||
// Preferences changes again
|
||||
sessionPreferencesStore.setRenderTypingNotifications(false)
|
||||
skipItems(2)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.renderTypingNotifications).isFalse()
|
||||
assertThat(finalState.typingMembers).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - state is updated when a member is typing, member is not known`() = runTest {
|
||||
val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = createPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.typingMembers).isEmpty()
|
||||
room.givenRoomTypingMembers(listOf(A_USER_ID_2))
|
||||
val oneMemberTypingState = awaitItem()
|
||||
assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1)
|
||||
assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember)
|
||||
// User stops typing
|
||||
room.givenRoomTypingMembers(emptyList())
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.typingMembers).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - state is updated when a member is typing, member is known`() = runTest {
|
||||
val aKnownRoomMember = createKnownRoomMember(userId = A_USER_ID_2)
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(
|
||||
MatrixRoomMembersState.Ready(
|
||||
listOf(
|
||||
createKnownRoomMember(A_USER_ID),
|
||||
aKnownRoomMember,
|
||||
createKnownRoomMember(A_USER_ID_3),
|
||||
createKnownRoomMember(A_USER_ID_4),
|
||||
).toImmutableList()
|
||||
)
|
||||
)
|
||||
}
|
||||
val presenter = createPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.typingMembers).isEmpty()
|
||||
room.givenRoomTypingMembers(listOf(A_USER_ID_2))
|
||||
val oneMemberTypingState = awaitItem()
|
||||
assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1)
|
||||
assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aKnownRoomMember)
|
||||
// User stops typing
|
||||
room.givenRoomTypingMembers(emptyList())
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.typingMembers).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - state is updated when a member is typing, member is not known, then known`() = runTest {
|
||||
val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2)
|
||||
val aKnownRoomMember = createKnownRoomMember(A_USER_ID_2)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = createPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.typingMembers).isEmpty()
|
||||
room.givenRoomTypingMembers(listOf(A_USER_ID_2))
|
||||
val oneMemberTypingState = awaitItem()
|
||||
assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1)
|
||||
assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember)
|
||||
// User is getting known
|
||||
room.givenRoomMembersState(
|
||||
MatrixRoomMembersState.Ready(
|
||||
listOf(aKnownRoomMember).toImmutableList()
|
||||
)
|
||||
)
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.typingMembers.first()).isEqualTo(aKnownRoomMember)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - reserveSpace becomes true once we get the first typing notification with room members`() = runTest {
|
||||
val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = createPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.typingMembers).isEmpty()
|
||||
room.givenRoomTypingMembers(listOf(A_USER_ID_2))
|
||||
skipItems(1)
|
||||
val updatedTypingState = awaitItem()
|
||||
assertThat(updatedTypingState.reserveSpace).isTrue()
|
||||
// User stops typing
|
||||
room.givenRoomTypingMembers(emptyList())
|
||||
// Is still true for all future events
|
||||
val futureEvents = cancelAndConsumeRemainingEvents()
|
||||
for (event in futureEvents) {
|
||||
if (event is Event.Item) {
|
||||
assertThat(event.value.reserveSpace).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
|
||||
givenRoomInfo(aRoomInfo(id = roomId.value, name = ""))
|
||||
},
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(
|
||||
isRenderTypingNotificationsEnabled = true
|
||||
),
|
||||
) = TypingNotificationPresenter(
|
||||
room = matrixRoom,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
|
||||
private fun createDefaultRoomMember(
|
||||
userId: UserId,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false,
|
||||
)
|
||||
|
||||
private fun createKnownRoomMember(
|
||||
userId: UserId,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
displayName = "Alice Doe",
|
||||
avatarUrl = "an_avatar_url",
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = true,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -37,7 +37,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||
@Composable
|
||||
fun ConnectivityIndicatorView(
|
||||
isOnline: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
|
||||
val isStatusBarPaddingVisible = remember { MutableTransitionState(isOnline) }.apply { targetState = isOnline }
|
||||
|
|
@ -48,7 +47,7 @@ fun ConnectivityIndicatorView(
|
|||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
Indicator(modifier)
|
||||
Indicator()
|
||||
}
|
||||
|
||||
// Show missing status bar padding when the indicator is not visible
|
||||
|
|
@ -57,7 +56,7 @@ fun ConnectivityIndicatorView(
|
|||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
StatusBarPaddingSpacer(modifier)
|
||||
StatusBarPaddingSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ internal fun Indicator(
|
|||
) {
|
||||
val tint = MaterialTheme.colorScheme.primary
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Offline,
|
||||
imageVector = CompoundIcons.Offline(),
|
||||
contentDescription = null,
|
||||
tint = tint,
|
||||
modifier = Modifier.size(16.sp.toDp()),
|
||||
|
|
|
|||
|
|
@ -93,10 +93,9 @@ fun OnBoardingView(
|
|||
private fun OnBoardingContent(
|
||||
state: OnBoardingState,
|
||||
onOpenDeveloperSettings: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
|
@ -143,7 +142,7 @@ private fun OnBoardingContent(
|
|||
onClick = onOpenDeveloperSettings,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.SettingsSolid,
|
||||
imageVector = CompoundIcons.SettingsSolid(),
|
||||
contentDescription = stringResource(CommonStrings.common_settings)
|
||||
)
|
||||
}
|
||||
|
|
@ -158,9 +157,8 @@ private fun OnBoardingButtons(
|
|||
onSignIn: () -> Unit,
|
||||
onCreateAccount: () -> Unit,
|
||||
onReportProblem: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ButtonColumnMolecule(modifier = modifier) {
|
||||
ButtonColumnMolecule {
|
||||
val signInButtonStringRes = if (state.canLoginWithQrCode || state.canCreateAccount) {
|
||||
R.string.screen_onboarding_sign_in_manually
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -143,21 +143,19 @@ fun PollContentView(
|
|||
private fun PollTitle(
|
||||
title: String,
|
||||
isPollEnded: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
if (isPollEnded) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.PollsEnd,
|
||||
imageVector = CompoundIcons.PollsEnd(),
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_poll_end),
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Polls,
|
||||
imageVector = CompoundIcons.Polls(),
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_poll),
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
|
|
@ -173,10 +171,9 @@ private fun PollTitle(
|
|||
private fun PollAnswers(
|
||||
answerItems: ImmutableList<PollAnswerItem>,
|
||||
onAnswerSelected: (PollAnswer) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.selectableGroup(),
|
||||
modifier = Modifier.selectableGroup(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
answerItems.forEach {
|
||||
|
|
@ -197,10 +194,9 @@ private fun PollAnswers(
|
|||
@Composable
|
||||
private fun ColumnScope.DisclosedPollBottomNotice(
|
||||
votesCount: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier.align(Alignment.End),
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
style = ElementTheme.typography.fontBodyXsRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
text = stringResource(CommonStrings.common_poll_total_votes, votesCount),
|
||||
|
|
@ -208,11 +204,9 @@ private fun ColumnScope.DisclosedPollBottomNotice(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.UndisclosedPollBottomNotice(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
private fun ColumnScope.UndisclosedPollBottomNotice() {
|
||||
Text(
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.align(Alignment.Start)
|
||||
.padding(start = 34.dp),
|
||||
style = ElementTheme.typography.fontBodyXsRegular,
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ fun CreatePollView(
|
|||
},
|
||||
trailingContent = ListItemContent.Custom {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Delete,
|
||||
imageVector = CompoundIcons.Delete(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable(answer.canDelete) {
|
||||
state.eventSink(CreatePollEvents.RemoveAnswer(index))
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ dependencies {
|
|||
implementation(projects.features.analytics.api)
|
||||
implementation(projects.features.ftue.api)
|
||||
implementation(projects.features.logout.api)
|
||||
implementation(projects.features.roomlist.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.datetime)
|
||||
|
|
|
|||
|
|
@ -92,9 +92,8 @@ fun DeveloperSettingsView(
|
|||
@Composable
|
||||
private fun ElementCallCategory(
|
||||
state: DeveloperSettingsState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
PreferenceCategory(modifier = modifier, title = "Element Call", showDivider = true) {
|
||||
PreferenceCategory(title = "Element Call", showDivider = true) {
|
||||
val callUrlState = state.customElementCallBaseUrlState
|
||||
fun isUsingDefaultUrl(value: String?): Boolean {
|
||||
return value.isNullOrEmpty() || value == callUrlState.defaultUrl
|
||||
|
|
@ -120,14 +119,12 @@ private fun ElementCallCategory(
|
|||
@Composable
|
||||
private fun FeatureListContent(
|
||||
state: DeveloperSettingsState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) {
|
||||
state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, isEnabled))
|
||||
}
|
||||
|
||||
FeatureListView(
|
||||
modifier = modifier,
|
||||
features = state.features,
|
||||
onCheckedChange = ::onFeatureEnabled,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ fun ConfigureTracingView(
|
|||
onClick = { showMenu = !showMenu }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.OverflowVertical,
|
||||
imageVector = CompoundIcons.OverflowVertical(),
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
|
@ -107,7 +107,7 @@ fun ConfigureTracingView(
|
|||
text = { Text("Reset to default") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Delete,
|
||||
imageVector = CompoundIcons.Delete(),
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
|
@ -141,14 +141,12 @@ fun ConfigureTracingView(
|
|||
@Composable
|
||||
private fun CrateListContent(
|
||||
state: ConfigureTracingState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
fun onLogLevelChange(target: Target, logLevel: LogLevel) {
|
||||
state.eventSink(ConfigureTracingEvents.UpdateFilter(target, logLevel))
|
||||
}
|
||||
|
||||
TargetAndLogLevelListView(
|
||||
modifier = modifier,
|
||||
data = state.targetsToLogLevel,
|
||||
onLogLevelChange = ::onLogLevelChange,
|
||||
)
|
||||
|
|
@ -158,11 +156,8 @@ private fun CrateListContent(
|
|||
private fun TargetAndLogLevelListView(
|
||||
data: ImmutableMap<Target, LogLevel>,
|
||||
onLogLevelChange: (Target, LogLevel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column {
|
||||
data.forEach { item ->
|
||||
fun onLogLevelChange(logLevel: LogLevel) {
|
||||
onLogLevelChange(item.key, logLevel)
|
||||
|
|
@ -182,10 +177,8 @@ private fun TargetAndLogLevelView(
|
|||
target: Target,
|
||||
logLevel: LogLevel,
|
||||
onLogLevelChange: (LogLevel) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = { Text(text = target.filter.takeIf { it.isNotEmpty() } ?: "(common)") },
|
||||
trailingContent = ListItemContent.Custom {
|
||||
LogLevelDropdownMenu(
|
||||
|
|
@ -200,10 +193,9 @@ private fun TargetAndLogLevelView(
|
|||
private fun LogLevelDropdownMenu(
|
||||
logLevel: LogLevel,
|
||||
onLogLevelChange: (LogLevel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = modifier) {
|
||||
Box {
|
||||
DropdownMenuItem(
|
||||
modifier = Modifier.widthIn(max = 120.dp),
|
||||
text = { Text(text = logLevel.filter) },
|
||||
|
|
@ -211,7 +203,7 @@ private fun LogLevelDropdownMenu(
|
|||
trailingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.rotate(if (expanded) 180f else 0f),
|
||||
imageVector = CompoundIcons.ChevronDown,
|
||||
imageVector = CompoundIcons.ChevronDown(),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue