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
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
path: |
|
||||
app/build/outputs/apk/gplay/debug/*.apk
|
||||
app/build/outputs/apk/fdroid/debug/*.apk
|
||||
- uses: rnkdsh/action-upload-diawi@v1.5.4
|
||||
- uses: rnkdsh/action-upload-diawi@v1.5.5
|
||||
id: diawi
|
||||
# Do not fail the whole build if Diawi upload fails
|
||||
continue-on-error: true
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ appId: ${APP_ID}
|
|||
# Back from timeline
|
||||
- back
|
||||
- assertVisible: "MyR"
|
||||
- hideKeyboard
|
||||
# Back from search
|
||||
- back
|
||||
- runFlow: ../assertions/assertHomeDisplayed.yaml
|
||||
|
|
|
|||
39
CHANGES.md
39
CHANGES.md
|
|
@ -1,3 +1,42 @@
|
|||
Changes in Element X v0.4.4 (2024-02-15)
|
||||
========================================
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
|
||||
- Fix decryption of previous messages after session verification not working.
|
||||
|
||||
Changes in Element X v0.4.3 (2024-02-14)
|
||||
========================================
|
||||
|
||||
Features ✨
|
||||
----------
|
||||
- Change "Read receipts" advanced setting used to send private Read Receipt to "Share presence" settings. When disabled, private Read Receipts will be sent, and no typing notification will be sent. Also Read Receipts and typing notifications will not be rendered in the timeline. ([#2241](https://github.com/element-hq/element-x-android/issues/2241))
|
||||
- Render typing notifications. ([#2242](https://github.com/element-hq/element-x-android/issues/2242))
|
||||
- Manually mark a room as unread. ([#2261](https://github.com/element-hq/element-x-android/issues/2261))
|
||||
- Add empty state to the room list. ([#2330](https://github.com/element-hq/element-x-android/issues/2330))
|
||||
- Allow joining unencrypted video calls in non encrypted rooms. ([#2333](https://github.com/element-hq/element-x-android/issues/2333))
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Fix crash after unregistering UnifiedPush distributor ([#2304](https://github.com/element-hq/element-x-android/issues/2304))
|
||||
- Add missing device id to settings screen. ([#2316](https://github.com/element-hq/element-x-android/issues/2316))
|
||||
- Open the keyboard (and keep it opened) when creating a poll. ([#2329](https://github.com/element-hq/element-x-android/issues/2329))
|
||||
- Fix message forwarding after SDK API change related to Timeline intitialization.
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Adjusted the login flow buttons so the continue button is always at the same height ([#825](https://github.com/element-hq/element-x-android/issues/825))
|
||||
- Move migration screen to within the room list ([#2310](https://github.com/element-hq/element-x-android/issues/2310))
|
||||
- Render correctly in reply to data when Event cannot be decrypted or has been redacted ([#2318](https://github.com/element-hq/element-x-android/issues/2318))
|
||||
- Remove Compose Foundation version pinning workaround. This was done to avoid a bug introduced in the default foundation version used by the material3 library, but that has already been fixed.
|
||||
- Remove `FilterHiddenStateEventsProcessor`, as this is already handled by the Rust SDK.
|
||||
- Remove session preferences on user log out.
|
||||
|
||||
Breaking changes 🚨
|
||||
-------------------
|
||||
- Update Compound icons in the project. Since the icon prefix changed to `ic_compound_` and the `CompoundIcons` helper now contains the vector icons as composable functions.
|
||||
|
||||
Changes in Element X v0.4.2 (2024-01-31)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -35,10 +35,8 @@ import io.element.android.x.R
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun IconPreview(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
internal fun IconPreview() {
|
||||
Box {
|
||||
Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null)
|
||||
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null)
|
||||
}
|
||||
|
|
@ -46,10 +44,8 @@ internal fun IconPreview(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoundIconPreview(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.clip(shape = CircleShape)) {
|
||||
internal fun RoundIconPreview() {
|
||||
Box(modifier = Modifier.clip(shape = CircleShape)) {
|
||||
Image(painter = painterResource(id = R.mipmap.ic_launcher_background), contentDescription = null)
|
||||
Image(painter = painterResource(id = R.mipmap.ic_launcher_foreground), contentDescription = null)
|
||||
}
|
||||
|
|
@ -57,11 +53,9 @@ internal fun RoundIconPreview(
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun MonochromeIconPreview(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
internal fun MonochromeIconPreview() {
|
||||
Box(
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.size(108.dp)
|
||||
.background(Color(0xFF2F3133))
|
||||
.clip(shape = RoundedCornerShape(32.dp)),
|
||||
|
|
|
|||
|
|
@ -84,10 +84,8 @@ fun LoadingRoomNodeView(
|
|||
@Composable
|
||||
private fun LoadingRoomTopBar(
|
||||
onBackClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClicked)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
|||
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
|
||||
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.apperror.api.AppErrorState
|
||||
import io.element.android.services.apperror.api.AppErrorStateService
|
||||
import io.element.android.services.apperror.impl.DefaultAppErrorStateService
|
||||
|
|
@ -83,6 +84,7 @@ class RootPresenterTest {
|
|||
val rageshake = FakeRageShake()
|
||||
val screenshotHolder = FakeScreenshotHolder()
|
||||
val crashDetectionPresenter = DefaultCrashDetectionPresenter(
|
||||
buildMeta = aBuildMeta(),
|
||||
crashDataStore = crashDataStore
|
||||
)
|
||||
val rageshakeDetectionPresenter = DefaultRageshakeDetectionPresenter(
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Fix message forwarding after SDK API change related to Timeline intitialization.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Remove Compose Foundation version pinning workaround. This was done to avoid a bug introduced in the default foundation version used by the material3 library, but that has already been fixed.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Remove session preferences on user log out.
|
||||
1
changelog.d/2208.feature
Normal file
1
changelog.d/2208.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
Mark a room or dm as favourite.
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
Change "Read receipts" advanced setting used to send private Read Receipt to "Share presence" settings.
|
||||
When disabled, private Read Receipts will be sent, and no typing notification will be sent. Also Read Receipts and typing notifications will not be rendered in the timeline.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix crash after unregistering UnifiedPush distributor
|
||||
|
|
@ -1 +0,0 @@
|
|||
Add missing device id to settings screen.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Add empty state to the room list.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Allow joining unencrypted video calls in non encrypted rooms.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Adjusted the login flow buttons so the continue button is always at the same height
|
||||
10
fastlane/metadata/android/en-US/changelogs/40004030.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/40004030.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Main changes in this version:
|
||||
- Added share presence toggle.
|
||||
- Render typing notifications.
|
||||
- Manually mark a room as unread.
|
||||
- Add an empty state to the room list.
|
||||
- Allow joining unencrypted video calls in non encrypted rooms.
|
||||
|
||||
And several other bugfixes.
|
||||
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
4
fastlane/metadata/android/en-US/changelogs/40004040.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/40004040.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Main changes in this version:
|
||||
- Fix decryption of previous messages after session verification not working.
|
||||
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
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