Merge branch 'develop' into feature/fga/permalink_timeline
This commit is contained in:
commit
a11bfb5d29
1157 changed files with 4307 additions and 1899 deletions
|
|
@ -219,6 +219,7 @@ dependencies {
|
||||||
allServicesImpl()
|
allServicesImpl()
|
||||||
allFeaturesImpl(rootDir, logger)
|
allFeaturesImpl(rootDir, logger)
|
||||||
implementation(projects.features.call)
|
implementation(projects.features.call)
|
||||||
|
implementation(projects.features.migration.api)
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
implementation(projects.appnav)
|
implementation(projects.appnav)
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ class MainActivity : NodeActivity() {
|
||||||
appBindings.preferencesStore().getThemeFlow().mapToTheme()
|
appBindings.preferencesStore().getThemeFlow().mapToTheme()
|
||||||
}
|
}
|
||||||
.collectAsState(initial = Theme.System)
|
.collectAsState(initial = Theme.System)
|
||||||
|
val migrationState = appBindings.migrationEntryPoint().present()
|
||||||
ElementTheme(
|
ElementTheme(
|
||||||
darkTheme = theme.isDark()
|
darkTheme = theme.isDark()
|
||||||
) {
|
) {
|
||||||
|
|
@ -98,19 +99,12 @@ class MainActivity : NodeActivity() {
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background),
|
.background(MaterialTheme.colorScheme.background),
|
||||||
) {
|
) {
|
||||||
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
if (migrationState.migrationAction.isSuccess()) {
|
||||||
MainNode(
|
MainNodeHost()
|
||||||
it,
|
} else {
|
||||||
plugins = listOf(
|
appBindings.migrationEntryPoint().Render(
|
||||||
object : NodeReadyObserver<MainNode> {
|
state = migrationState,
|
||||||
override fun init(node: MainNode) {
|
modifier = Modifier,
|
||||||
Timber.tag(loggerTag.value).w("onMainNodeInit")
|
|
||||||
mainNode = node
|
|
||||||
mainNode.handleIntent(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
context = applicationContext
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,6 +112,25 @@ class MainActivity : NodeActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MainNodeHost() {
|
||||||
|
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
||||||
|
MainNode(
|
||||||
|
it,
|
||||||
|
plugins = listOf(
|
||||||
|
object : NodeReadyObserver<MainNode> {
|
||||||
|
override fun init(node: MainNode) {
|
||||||
|
Timber.tag(loggerTag.value).w("onMainNodeInit")
|
||||||
|
mainNode = node
|
||||||
|
mainNode.handleIntent(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
context = applicationContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when:
|
* Called when:
|
||||||
* - the launcher icon is clicked (if the app is already running);
|
* - the launcher icon is clicked (if the app is already running);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.x.di
|
package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import io.element.android.features.api.MigrationEntryPoint
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
|
|
@ -35,4 +36,6 @@ interface AppBindings {
|
||||||
fun lockScreenService(): LockScreenService
|
fun lockScreenService(): LockScreenService
|
||||||
|
|
||||||
fun preferencesStore(): AppPreferencesStore
|
fun preferencesStore(): AppPreferencesStore
|
||||||
|
|
||||||
|
fun migrationEntryPoint(): MigrationEntryPoint
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,9 @@
|
||||||
package io.element.android.appnav.room
|
package io.element.android.appnav.room
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
|
@ -36,7 +35,11 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
|
import io.element.android.appnav.room.joined.LoadingRoomNodeView
|
||||||
|
import io.element.android.appnav.room.joined.LoadingRoomState
|
||||||
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -44,7 +47,6 @@ import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
|
@ -69,6 +71,7 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
private val roomMembershipObserver: RoomMembershipObserver,
|
private val roomMembershipObserver: RoomMembershipObserver,
|
||||||
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
private val joinRoomEntryPoint: JoinRoomEntryPoint,
|
||||||
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
|
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
) : BaseFlowNode<RoomFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Loading,
|
initialElement = NavTarget.Loading,
|
||||||
|
|
@ -125,7 +128,15 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
|
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
|
||||||
val info = roomInfo.getOrNull()
|
val info = roomInfo.getOrNull()
|
||||||
if (info?.currentUserMembership == CurrentUserMembership.JOINED) {
|
if (info?.currentUserMembership == CurrentUserMembership.JOINED) {
|
||||||
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
if (info.isSpace) {
|
||||||
|
// It should not happen, but probably due to an issue in the sliding sync,
|
||||||
|
// we can have a space here in case the space has just been joined.
|
||||||
|
// So navigate to the JoinRoom target for now, which will
|
||||||
|
// handle the space not supported screen
|
||||||
|
backstack.newRoot(NavTarget.JoinRoom(roomId))
|
||||||
|
} else {
|
||||||
|
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
backstack.newRoot(NavTarget.JoinRoom(roomId))
|
backstack.newRoot(NavTarget.JoinRoom(roomId))
|
||||||
}
|
}
|
||||||
|
|
@ -175,10 +186,14 @@ class RoomFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadingNode(buildContext: BuildContext) = node(buildContext) {
|
private fun loadingNode(buildContext: BuildContext) = node(buildContext) { modifier ->
|
||||||
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
|
val networkStatus by networkMonitor.connectivity.collectAsState()
|
||||||
CircularProgressIndicator()
|
LoadingRoomNodeView(
|
||||||
}
|
state = LoadingRoomState.Loading,
|
||||||
|
hasNetworkConnection = networkStatus == NetworkStatus.Online,
|
||||||
|
onBackClicked = { navigateUp() },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ fun RootView(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RootPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
||||||
RootView(
|
RootView(
|
||||||
state = rootState,
|
state = rootState,
|
||||||
onOpenBugReport = {},
|
onOpenBugReport = {},
|
||||||
|
|
|
||||||
1
changelog.d/2718.bugfix
Normal file
1
changelog.d/2718.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix session verification being asked again for already verified users.
|
||||||
1
changelog.d/2740.bugfix
Normal file
1
changelog.d/2740.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Instead of displaying 'create new recovery key' on the session verification screen when there is no other session active, display it always under the 'enter recovery key' screen.
|
||||||
1
changelog.d/2749.misc
Normal file
1
changelog.d/2749.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Migrate application data.
|
||||||
|
|
@ -43,8 +43,8 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo
|
||||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
|
||||||
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.background.OnboardingBackground
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.components.OnboardingBackground
|
|
||||||
import io.element.android.libraries.designsystem.components.PageTitle
|
import io.element.android.libraries.designsystem.components.PageTitle
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.features.analytics.api)
|
implementation(projects.features.analytics.api)
|
||||||
|
|
@ -60,6 +61,7 @@ dependencies {
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.libraries.permissions.impl)
|
testImplementation(projects.libraries.permissions.impl)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.features.lockscreen.test)
|
testImplementation(projects.features.lockscreen.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun moveToNextStep() {
|
private fun moveToNextStep() = lifecycleScope.launch {
|
||||||
when (ftueState.getNextStep()) {
|
when (ftueState.getNextStep()) {
|
||||||
FtueStep.SessionVerification -> {
|
FtueStep.SessionVerification -> {
|
||||||
backstack.newRoot(NavTarget.SessionVerification)
|
backstack.newRoot(NavTarget.SessionVerification)
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.ftue.impl.R
|
import io.element.android.features.ftue.impl.R
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.background.OnboardingBackground
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
import io.element.android.libraries.designsystem.components.OnboardingBackground
|
|
||||||
import io.element.android.libraries.designsystem.components.PageTitle
|
import io.element.android.libraries.designsystem.components.PageTitle
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,6 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object EnterRecoveryKey : NavTarget
|
data object EnterRecoveryKey : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object CreateNewRecoveryKey : NavTarget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
|
|
@ -68,10 +65,6 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback {
|
private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback {
|
||||||
override fun onCreateNewRecoveryKey() {
|
|
||||||
backstack.push(NavTarget.CreateNewRecoveryKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Move to the completed state view in the verification flow
|
// Move to the completed state view in the verification flow
|
||||||
|
|
@ -89,10 +82,6 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.EnterRecoveryKey)
|
backstack.push(NavTarget.EnterRecoveryKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateNewRecoveryKey() {
|
|
||||||
backstack.push(NavTarget.CreateNewRecoveryKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
plugins<Callback>().forEach { it.onDone() }
|
plugins<Callback>().forEach { it.onDone() }
|
||||||
}
|
}
|
||||||
|
|
@ -105,12 +94,6 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor(
|
||||||
.callback(secureBackupEntryPointCallback)
|
.callback(secureBackupEntryPointCallback)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
is NavTarget.CreateNewRecoveryKey -> {
|
|
||||||
secureBackupEntryPoint.nodeBuilder(this, buildContext)
|
|
||||||
.params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey))
|
|
||||||
.callback(secureBackupEntryPointCallback)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,26 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.timeout
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFtueService @Inject constructor(
|
class DefaultFtueService @Inject constructor(
|
||||||
|
|
@ -44,6 +52,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
private val permissionStateProvider: PermissionStateProvider,
|
private val permissionStateProvider: PermissionStateProvider,
|
||||||
private val lockScreenService: LockScreenService,
|
private val lockScreenService: LockScreenService,
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : FtueService {
|
) : FtueService {
|
||||||
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
||||||
|
|
||||||
|
|
@ -55,7 +64,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
sessionVerificationService.needsVerificationFlow
|
sessionVerificationService.sessionVerifiedStatus
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
|
|
||||||
|
|
@ -64,7 +73,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||||
when (currentStep) {
|
when (currentStep) {
|
||||||
null -> if (isSessionNotVerified()) {
|
null -> if (isSessionNotVerified()) {
|
||||||
FtueStep.SessionVerification
|
FtueStep.SessionVerification
|
||||||
|
|
@ -89,8 +98,8 @@ class DefaultFtueService @Inject constructor(
|
||||||
FtueStep.AnalyticsOptIn -> null
|
FtueStep.AnalyticsOptIn -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAnyStepIncomplete(): Boolean {
|
private suspend fun isAnyStepIncomplete(): Boolean {
|
||||||
return listOf(
|
return listOf<suspend () -> Boolean>(
|
||||||
{ isSessionNotVerified() },
|
{ isSessionNotVerified() },
|
||||||
{ shouldAskNotificationPermissions() },
|
{ shouldAskNotificationPermissions() },
|
||||||
{ needsAnalyticsOptIn() },
|
{ needsAnalyticsOptIn() },
|
||||||
|
|
@ -98,16 +107,28 @@ class DefaultFtueService @Inject constructor(
|
||||||
).any { it() }
|
).any { it() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSessionNotVerified(): Boolean {
|
@OptIn(FlowPreview::class)
|
||||||
return sessionVerificationService.needsVerificationFlow.value
|
private suspend fun isSessionNotVerified(): Boolean {
|
||||||
|
// Wait for the first known (or ready) verification status
|
||||||
|
val readyVerifiedSessionStatus = sessionVerificationService.sessionVerifiedStatus
|
||||||
|
.filter { it != SessionVerifiedStatus.Unknown }
|
||||||
|
// This is not ideal, but there are some very rare cases when reading the flow seems to get stuck
|
||||||
|
.timeout(5.seconds)
|
||||||
|
.catch {
|
||||||
|
Timber.e(it, "Failed to get session verification status, assume it's not verified")
|
||||||
|
emit(SessionVerifiedStatus.NotVerified)
|
||||||
|
}
|
||||||
|
.first()
|
||||||
|
val skipVerification = suspend { sessionPreferencesStore.isSessionVerificationSkipped().first() }
|
||||||
|
return readyVerifiedSessionStatus == SessionVerifiedStatus.NotVerified && !skipVerification()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun needsAnalyticsOptIn(): Boolean {
|
private suspend fun needsAnalyticsOptIn(): Boolean {
|
||||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
return analyticsService.didAskUserConsent().first().not()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldAskNotificationPermissions(): Boolean {
|
private suspend fun shouldAskNotificationPermissions(): Boolean {
|
||||||
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||||
val permission = Manifest.permission.POST_NOTIFICATIONS
|
val permission = Manifest.permission.POST_NOTIFICATIONS
|
||||||
val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() }
|
val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() }
|
||||||
|
|
@ -118,14 +139,12 @@ class DefaultFtueService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldDisplayLockscreenSetup(): Boolean {
|
private suspend fun shouldDisplayLockscreenSetup(): Boolean {
|
||||||
return runBlocking {
|
return lockScreenService.isSetupRequired().first()
|
||||||
lockScreenService.isSetupRequired().first()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun updateState() {
|
internal suspend fun updateState() {
|
||||||
state.value = when {
|
state.value = when {
|
||||||
isAnyStepIncomplete() -> FtueState.Incomplete
|
isAnyStepIncomplete() -> FtueState.Incomplete
|
||||||
else -> FtueState.Complete
|
else -> FtueState.Complete
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Няправільны QR-код"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Няправільны QR-код"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Перайсці ў налады камеры"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Перайсці ў налады камеры"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Каб працягнуць, вам неабходна дазволіць Element выкарыстоўваць камеру вашай прылады."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Каб працягнуць, вам неабходна дазволіць %1$s выкарыстоўваць камеру вашай прылады."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Дазвольце доступ да камеры для сканавання QR-кода"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Дазвольце доступ да камеры для сканавання QR-кода"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Сканаваць QR-код"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Сканаваць QR-код"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Пачаць спачатку"</string>
|
<string name="screen_qr_code_login_start_over_button">"Пачаць спачатку"</string>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Špatný QR kód"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Špatný QR kód"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Přejděte na nastavení fotoaparátu"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Přejděte na nastavení fotoaparátu"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Abyste mohli pokračovat, musíte aplikaci Element udělit povolení k použití kamery vašeho zařízení."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Abyste mohli pokračovat, musíte aplikaci %1$s udělit povolení k použití kamery vašeho zařízení."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povolte přístup k fotoaparátu a naskenujte QR kód"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povolte přístup k fotoaparátu a naskenujte QR kód"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Naskenujte QR kód"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Naskenujte QR kód"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Začít znovu"</string>
|
<string name="screen_qr_code_login_start_over_button">"Začít znovu"</string>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Wenn das nicht funktioniert, melde dich manuell an"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Wenn das nicht funktioniert, melde dich manuell an"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Nummer auf deinem Gerät eingeben"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
|
||||||
|
|
@ -20,11 +22,14 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Falscher QR-Code"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Falscher QR-Code"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Gehe zu den Kameraeinstellungen"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Gehe zu den Kameraeinstellungen"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du musst Element die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Du musst %1$s die Erlaubnis erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"QR-Code scannen"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"QR-Code scannen"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Neu beginnen"</string>
|
<string name="screen_qr_code_login_start_over_button">"Neu beginnen"</string>
|
||||||
<string name="screen_qr_code_login_unknown_error_description">"Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."</string>
|
<string name="screen_qr_code_login_unknown_error_description">"Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Warten auf dein anderes Gerät"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Dein Account-Provider kann nach dem folgenden Code fragen, um die Anmeldung zu bestätigen."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Dein Verifizierungscode"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
|
<string name="screen_welcome_bullet_1">"Anrufe, Umfragen, Suchfunktionen und mehr werden im Laufe des Jahres hinzugefügt."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
<string name="screen_welcome_bullet_2">"Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
|
<string name="screen_welcome_bullet_3">"Wir würden uns freuen, von dir zu hören. Teile uns deine Meinung über die Einstellungsseite mit."</string>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ha ugyanezzel a problémával találkozik, próbálkozzon másik Wi-Fi-hálózattal, vagy a Wi-Fi helyett használja a mobil-adatkapcsolatát"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Ha ugyanezzel a problémával találkozik, próbálkozzon másik Wi-Fi-hálózattal, vagy a Wi-Fi helyett használja a mobil-adatkapcsolatát"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ha ez nem működik, jelentkezzen be kézileg"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Ha ez nem működik, jelentkezzen be kézileg"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Adja meg a számot az eszközén"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
|
||||||
|
|
@ -20,11 +22,14 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Hibás QR-kód"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Hibás QR-kód"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Ugrás a kamerabeállításokhoz"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Ugrás a kamerabeállításokhoz"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"A folytatáshoz engedélyeznie kell, hogy az Element használhassa az eszköz kameráját."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"A folytatáshoz engedélyeznie kell, hogy az %1$s használhassa az eszköz kameráját."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Engedélyezze a kamera elérését a QR-kód beolvasásához"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Engedélyezze a kamera elérését a QR-kód beolvasásához"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Olvassa be a QR-kódot"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Olvassa be a QR-kódot"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Újrakezdés"</string>
|
<string name="screen_qr_code_login_start_over_button">"Újrakezdés"</string>
|
||||||
<string name="screen_qr_code_login_unknown_error_description">"Váratlan hiba történt. Próbálja meg újra."</string>
|
<string name="screen_qr_code_login_unknown_error_description">"Váratlan hiba történt. Próbálja meg újra."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Várakozás a másik eszközre"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"A fiókszolgáltatója kérheti a következő kódot a bejelentkezése ellenőrzéséhez."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Az Ön ellenőrzőkódja"</string>
|
||||||
<string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string>
|
<string name="screen_welcome_bullet_1">"A hívások, szavazások, keresések és egyebek az év további részében kerülnek hozzáadásra."</string>
|
||||||
<string name="screen_welcome_bullet_2">"A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."</string>
|
<string name="screen_welcome_bullet_2">"A titkosított szobák üzenetelőzményei nem lesznek elérhetők ebben a frissítésben."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Szeretnénk hallani a véleményét, ossza meg velünk a beállítások oldalon."</string>
|
<string name="screen_welcome_bullet_3">"Szeretnénk hallani a véleményét, ossza meg velünk a beállítások oldalon."</string>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,14 @@
|
||||||
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
|
<string name="screen_notification_optin_subtitle">"Anda dapat mengubah pengaturan Anda nanti."</string>
|
||||||
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
|
<string name="screen_notification_optin_title">"Izinkan pemberitahuan dan jangan pernah melewatkan pesan"</string>
|
||||||
<string name="screen_qr_code_login_connecting_subtitle">"Membuat koneksi"</string>
|
<string name="screen_qr_code_login_connecting_subtitle">"Membuat koneksi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Koneksi aman tidak dapat dibuat ke perangkat baru. Perangkat Anda yang ada masih aman dan Anda tidak perlu khawatir tentang mereka."</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Apa sekarang?"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Coba masuk lagi dengan kode QR jika ini adalah masalah jaringan"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Jika Anda mengalami masalah yang sama, coba jaringan Wi-Fi yang berbeda atau gunakan data seluler Anda daripada Wi-Fi"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Jika tidak berhasil, masuk secara manual"</string>
|
||||||
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Koneksi tidak aman"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Anda akan diminta untuk memasukkan dua digit yang ditunjukkan di bawah ini."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Masukkan nomor di perangkat Anda"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Buka %1$s di perangkat desktop"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Buka %1$s di perangkat desktop"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
|
||||||
|
|
@ -13,7 +21,15 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
|
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Kode QR salah"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Kode QR salah"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_button">"Pergi ke pengaturan kamera"</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Anda perlu memberikan izin ke %1$s untuk menggunakan kamera perangkat Anda untuk melanjutkan."</string>
|
||||||
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Izinkan akses kamera untuk memindai kode QR"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Pindai kode QR"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Pindai kode QR"</string>
|
||||||
|
<string name="screen_qr_code_login_start_over_button">"Mulai dari awal"</string>
|
||||||
|
<string name="screen_qr_code_login_unknown_error_description">"Terjadi kesalahan tak terduga. Silakan coba lagi."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"Menunggu perangkat Anda yang lain"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Penyedia akun Anda mungkin meminta kode berikut untuk memverifikasi proses masuk."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Kode verifikasi Anda"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string>
|
<string name="screen_welcome_bullet_1">"Panggilan, pemungutan suara, pencarian, dan lainnya akan ditambahkan di tahun ini."</string>
|
||||||
<string name="screen_welcome_bullet_2">"Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."</string>
|
<string name="screen_welcome_bullet_2">"Riwayat pesan untuk ruangan terenkripsi tidak akan tersedia dalam pembaruan ini."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Kami ingin mendengar dari Anda, beri tahu kami pendapat Anda melalui halaman pengaturan."</string>
|
<string name="screen_welcome_bullet_3">"Kami ingin mendengar dari Anda, beri tahu kami pendapat Anda melalui halaman pengaturan."</string>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_subtitle">"Вам будет предложено ввести две цифры, показанные ниже."</string>
|
||||||
|
<string name="screen_qr_code_login_device_code_title">"Введите номер на своем устройстве"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
|
||||||
|
|
@ -20,11 +22,14 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Неверный QR-код"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Неверный QR-код"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Перейдите в настройки камеры"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Перейдите в настройки камеры"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Чтобы продолжить, вам необходимо разрешить Element использовать камеру вашего устройства."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Чтобы продолжить, вам необходимо разрешить %1$s использовать камеру вашего устройства."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Разрешите доступ к камере для сканирования QR-кода"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Разрешите доступ к камере для сканирования QR-кода"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Сканировать QR-код"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Сканировать QR-код"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Начать заново"</string>
|
<string name="screen_qr_code_login_start_over_button">"Начать заново"</string>
|
||||||
<string name="screen_qr_code_login_unknown_error_description">"Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз."</string>
|
<string name="screen_qr_code_login_unknown_error_description">"Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_loading">"В ожидании другого устройства"</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_subtitle">"Поставщик учетной записи может запросить следующий код для подтверждения входа."</string>
|
||||||
|
<string name="screen_qr_code_login_verify_code_title">"Ваш код подтверждения"</string>
|
||||||
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
<string name="screen_welcome_bullet_1">"Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."</string>
|
||||||
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
<string name="screen_welcome_bullet_2">"История сообщений для зашифрованных комнат в этом обновлении будет недоступна."</string>
|
||||||
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
<string name="screen_welcome_bullet_3">"Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."</string>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Nesprávny QR kód"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Nesprávny QR kód"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Prejsť na nastavenia fotoaparátu"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Prejsť na nastavenia fotoaparátu"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"Ak chcete pokračovať, musíte udeliť povolenie aplikácii Element používať fotoaparát vášho zariadenia."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"Ak chcete pokračovať, musíte udeliť povolenie aplikácii %1$s používať fotoaparát vášho zariadenia."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povoľte prístup k fotoaparátu na naskenovanie QR kódu"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Povoľte prístup k fotoaparátu na naskenovanie QR kódu"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Naskenovať QR kód"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Naskenovať QR kód"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Začať odznova"</string>
|
<string name="screen_qr_code_login_start_over_button">"Začať odznova"</string>
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesn’t work, sign in manually"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesn’t work, sign in manually"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown below."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"You’ll be asked to enter the two digits shown on this device."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Enter number on your device"</string>
|
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>
|
||||||
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Wrong QR code"</string>
|
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Wrong QR code"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_button">"Go to camera settings"</string>
|
<string name="screen_qr_code_login_no_camera_permission_button">"Go to camera settings"</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for Element to use your device’s camera in order to continue."</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for %1$s to use your device’s camera in order to continue."</string>
|
||||||
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
|
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
|
||||||
<string name="screen_qr_code_login_scanning_state_title">"Scan the QR code"</string>
|
<string name="screen_qr_code_login_scanning_state_title">"Scan the QR code"</string>
|
||||||
<string name="screen_qr_code_login_start_over_button">"Start over"</string>
|
<string name="screen_qr_code_login_start_over_button">"Start over"</string>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||||
|
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||||
|
|
@ -90,7 +91,6 @@ class DefaultFtueServiceTests {
|
||||||
fun `traverse flow`() = runTest {
|
fun `traverse flow`() = runTest {
|
||||||
val sessionVerificationService = FakeSessionVerificationService().apply {
|
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||||
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
givenNeedsVerification(true)
|
|
||||||
}
|
}
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||||
|
|
@ -108,7 +108,7 @@ class DefaultFtueServiceTests {
|
||||||
|
|
||||||
// Session verification
|
// Session verification
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
sessionVerificationService.givenNeedsVerification(false)
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
|
|
||||||
// Notifications opt in
|
// Notifications opt in
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
|
@ -200,6 +200,7 @@ class DefaultFtueServiceTests {
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||||
lockScreenService: LockScreenService = FakeLockScreenService(),
|
lockScreenService: LockScreenService = FakeLockScreenService(),
|
||||||
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
// First version where notification permission is required
|
// First version where notification permission is required
|
||||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||||
) = DefaultFtueService(
|
) = DefaultFtueService(
|
||||||
|
|
@ -209,5 +210,6 @@ class DefaultFtueServiceTests {
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,14 @@ class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch {
|
||||||
acceptedAction.runUpdatingState {
|
acceptedAction.runUpdatingState {
|
||||||
client.joinRoom(roomId).onSuccess {
|
client.joinRoom(roomId)
|
||||||
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
.onSuccess {
|
||||||
client.getRoom(roomId)?.use { room ->
|
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
|
||||||
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
client.getRoom(roomId)?.use { room ->
|
||||||
|
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.map { roomId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - accepting invite error flow`() = runTest {
|
fun `present - accepting invite error flow`() = runTest {
|
||||||
val joinRoomFailure = lambdaRecorder { roomId: RoomId ->
|
val joinRoomFailure = lambdaRecorder { roomId: RoomId ->
|
||||||
Result.failure<RoomId>(RuntimeException("Failed to join room $roomId"))
|
Result.failure<Unit>(RuntimeException("Failed to join room $roomId"))
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
joinRoomLambda = joinRoomFailure
|
joinRoomLambda = joinRoomFailure
|
||||||
|
|
@ -197,8 +197,8 @@ class AcceptDeclineInvitePresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - accepting invite success flow`() = runTest {
|
fun `present - accepting invite success flow`() = runTest {
|
||||||
val joinRoomSuccess = lambdaRecorder { roomId: RoomId ->
|
val joinRoomSuccess = lambdaRecorder { _: RoomId ->
|
||||||
Result.success(roomId)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
joinRoomLambda = joinRoomSuccess
|
joinRoomLambda = joinRoomSuccess
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.joinroom.impl"
|
namespace = "io.element.android.features.joinroom.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -46,10 +51,13 @@ dependencies {
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ package io.element.android.features.joinroom.impl
|
||||||
sealed interface JoinRoomEvents {
|
sealed interface JoinRoomEvents {
|
||||||
data object RetryFetchingContent : JoinRoomEvents
|
data object RetryFetchingContent : JoinRoomEvents
|
||||||
data object JoinRoom : JoinRoomEvents
|
data object JoinRoom : JoinRoomEvents
|
||||||
|
data object KnockRoom : JoinRoomEvents
|
||||||
|
data object ClearError : JoinRoomEvents
|
||||||
data object AcceptInvite : JoinRoomEvents
|
data object AcceptInvite : JoinRoomEvents
|
||||||
data object DeclineInvite : JoinRoomEvents
|
data object DeclineInvite : JoinRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ class JoinRoomNode @AssistedInject constructor(
|
||||||
JoinRoomView(
|
JoinRoomView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackPressed = ::navigateUp,
|
onBackPressed = ::navigateUp,
|
||||||
|
onKnockSuccess = ::navigateUp,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
acceptDeclineInviteView.Render(
|
acceptDeclineInviteView.Render(
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,37 @@ package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
import io.element.android.features.invite.api.response.InviteData
|
||||||
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
class JoinRoomPresenter @AssistedInject constructor(
|
class JoinRoomPresenter @AssistedInject constructor(
|
||||||
|
|
@ -45,7 +56,9 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
|
||||||
@Assisted private val roomDescription: Optional<RoomDescription>,
|
@Assisted private val roomDescription: Optional<RoomDescription>,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
|
private val knockRoom: KnockRoom,
|
||||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
private val buildMeta: BuildMeta,
|
||||||
) : Presenter<JoinRoomState> {
|
) : Presenter<JoinRoomState> {
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
@ -57,8 +70,10 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): JoinRoomState {
|
override fun present(): JoinRoomState {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var retryCount by remember { mutableIntStateOf(0) }
|
var retryCount by remember { mutableIntStateOf(0) }
|
||||||
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
|
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
|
||||||
|
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
val contentState by produceState<ContentState>(
|
val contentState by produceState<ContentState>(
|
||||||
initialValue = ContentState.Loading(roomIdOrAlias),
|
initialValue = ContentState.Loading(roomIdOrAlias),
|
||||||
key1 = roomInfo,
|
key1 = roomInfo,
|
||||||
|
|
@ -75,7 +90,9 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
value = ContentState.Loading(roomIdOrAlias)
|
value = ContentState.Loading(roomIdOrAlias)
|
||||||
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias())
|
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias())
|
||||||
value = result.fold(
|
value = result.fold(
|
||||||
onSuccess = { it.toContentState() },
|
onSuccess = { roomPreview ->
|
||||||
|
roomPreview.toContentState()
|
||||||
|
},
|
||||||
onFailure = { throwable ->
|
onFailure = { throwable ->
|
||||||
if (throwable.message?.contains("403") == true) {
|
if (throwable.message?.contains("403") == true) {
|
||||||
ContentState.UnknownRoom(roomIdOrAlias)
|
ContentState.UnknownRoom(roomIdOrAlias)
|
||||||
|
|
@ -98,6 +115,9 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
JoinRoomEvents.KnockRoom -> {
|
||||||
|
coroutineScope.knockRoom(roomId, knockAction)
|
||||||
|
}
|
||||||
JoinRoomEvents.DeclineInvite -> {
|
JoinRoomEvents.DeclineInvite -> {
|
||||||
val inviteData = contentState.toInviteData() ?: return
|
val inviteData = contentState.toInviteData() ?: return
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
|
|
@ -107,15 +127,26 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
JoinRoomEvents.RetryFetchingContent -> {
|
JoinRoomEvents.RetryFetchingContent -> {
|
||||||
retryCount++
|
retryCount++
|
||||||
}
|
}
|
||||||
|
JoinRoomEvents.ClearError -> {
|
||||||
|
knockAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return JoinRoomState(
|
return JoinRoomState(
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
|
knockAction = knockAction.value,
|
||||||
|
applicationName = buildMeta.applicationName,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.knockRoom(roomId: RoomId, knockAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
|
knockAction.runUpdatingState {
|
||||||
|
knockRoom(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomPreview.toContentState(): ContentState {
|
private fun RoomPreview.toContentState(): ContentState {
|
||||||
|
|
@ -126,9 +157,11 @@ private fun RoomPreview.toContentState(): ContentState {
|
||||||
alias = canonicalAlias,
|
alias = canonicalAlias,
|
||||||
numberOfMembers = numberOfJoinedMembers,
|
numberOfMembers = numberOfJoinedMembers,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
|
roomType = roomType,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when {
|
joinAuthorisationStatus = when {
|
||||||
isInvited -> JoinAuthorisationStatus.IsInvited
|
// Note when isInvited, roomInfo will be used, so if this happen, it will be temporary.
|
||||||
|
isInvited -> JoinAuthorisationStatus.IsInvited(null)
|
||||||
canKnock -> JoinAuthorisationStatus.CanKnock
|
canKnock -> JoinAuthorisationStatus.CanKnock
|
||||||
isPublic -> JoinAuthorisationStatus.CanJoin
|
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||||
else -> JoinAuthorisationStatus.Unknown
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
|
|
@ -145,6 +178,7 @@ internal fun RoomDescription.toContentState(): ContentState {
|
||||||
alias = alias,
|
alias = alias,
|
||||||
numberOfMembers = numberOfMembers,
|
numberOfMembers = numberOfMembers,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
|
roomType = RoomType.Room,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when (joinRule) {
|
joinAuthorisationStatus = when (joinRule) {
|
||||||
RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock
|
RoomDescription.JoinRule.KNOCK -> JoinAuthorisationStatus.CanKnock
|
||||||
|
|
@ -163,9 +197,12 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
||||||
alias = canonicalAlias,
|
alias = canonicalAlias,
|
||||||
numberOfMembers = activeMembersCount,
|
numberOfMembers = activeMembersCount,
|
||||||
isDirect = isDirect,
|
isDirect = isDirect,
|
||||||
|
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when {
|
joinAuthorisationStatus = when {
|
||||||
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited
|
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
||||||
|
inviteSender = inviter?.toInviteSender()
|
||||||
|
)
|
||||||
isPublic -> JoinAuthorisationStatus.CanJoin
|
isPublic -> JoinAuthorisationStatus.CanJoin
|
||||||
else -> JoinAuthorisationStatus.Unknown
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +214,8 @@ internal fun ContentState.toInviteData(): InviteData? {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is ContentState.Loaded -> InviteData(
|
is ContentState.Loaded -> InviteData(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
roomName = computedTitle,
|
// Note: name should not be null at this point, but use Id just in case...
|
||||||
|
roomName = name ?: roomId.value,
|
||||||
isDirect = isDirect
|
isDirect = isDirect
|
||||||
)
|
)
|
||||||
else -> null
|
else -> null
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,21 @@ package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class JoinRoomState(
|
data class JoinRoomState(
|
||||||
val contentState: ContentState,
|
val contentState: ContentState,
|
||||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||||
|
val knockAction: AsyncAction<Unit>,
|
||||||
|
val applicationName: String,
|
||||||
val eventSink: (JoinRoomEvents) -> Unit
|
val eventSink: (JoinRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val joinAuthorisationStatus = when (contentState) {
|
val joinAuthorisationStatus = when (contentState) {
|
||||||
|
|
@ -47,17 +52,10 @@ sealed interface ContentState {
|
||||||
val alias: RoomAlias?,
|
val alias: RoomAlias?,
|
||||||
val numberOfMembers: Long?,
|
val numberOfMembers: Long?,
|
||||||
val isDirect: Boolean,
|
val isDirect: Boolean,
|
||||||
|
val roomType: RoomType,
|
||||||
val roomAvatarUrl: String?,
|
val roomAvatarUrl: String?,
|
||||||
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
val joinAuthorisationStatus: JoinAuthorisationStatus,
|
||||||
) : ContentState {
|
) : ContentState {
|
||||||
val computedTitle = name ?: roomId.value
|
|
||||||
|
|
||||||
val computedSubtitle = when {
|
|
||||||
alias != null -> alias.value
|
|
||||||
name == null -> ""
|
|
||||||
else -> roomId.value
|
|
||||||
}
|
|
||||||
|
|
||||||
val showMemberCount = numberOfMembers != null
|
val showMemberCount = numberOfMembers != null
|
||||||
|
|
||||||
fun avatarData(size: AvatarSize): AvatarData {
|
fun avatarData(size: AvatarSize): AvatarData {
|
||||||
|
|
@ -71,9 +69,9 @@ sealed interface ContentState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class JoinAuthorisationStatus {
|
sealed interface JoinAuthorisationStatus {
|
||||||
IsInvited,
|
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
||||||
CanKnock,
|
data object CanKnock : JoinAuthorisationStatus
|
||||||
CanJoin,
|
data object CanJoin : JoinAuthorisationStatus
|
||||||
Unknown,
|
data object Unknown : JoinAuthorisationStatus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,16 @@ package io.element.android.features.joinroom.impl
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
|
|
||||||
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
override val values: Sequence<JoinRoomState>
|
override val values: Sequence<JoinRoomState>
|
||||||
|
|
@ -33,6 +39,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = anUnknownContentState()
|
contentState = anUnknownContentState()
|
||||||
),
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
name = null,
|
||||||
|
alias = null,
|
||||||
|
topic = null,
|
||||||
|
)
|
||||||
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
|
||||||
),
|
),
|
||||||
|
|
@ -48,7 +61,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited)
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null))
|
||||||
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
numberOfMembers = 123,
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(anInviteSender()),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aFailureContentState()
|
contentState = aFailureContentState()
|
||||||
|
|
@ -56,6 +75,15 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias())
|
contentState = aFailureContentState(roomIdOrAlias = A_ROOM_ALIAS.toRoomIdOrAlias())
|
||||||
),
|
),
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(
|
||||||
|
roomId = RoomId("!aSpaceId:domain"),
|
||||||
|
name = "A space",
|
||||||
|
alias = null,
|
||||||
|
topic = "This is the topic of a space",
|
||||||
|
roomType = RoomType.Space,
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,11 +102,12 @@ fun aLoadingContentState(roomId: RoomId = A_ROOM_ID) = ContentState.Loading(room
|
||||||
|
|
||||||
fun aLoadedContentState(
|
fun aLoadedContentState(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
name: String = "Element X android",
|
name: String? = "Element X android",
|
||||||
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
|
alias: RoomAlias? = RoomAlias("#exa:matrix.org"),
|
||||||
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
topic: String? = "Element X is a secure, private and decentralized messenger.",
|
||||||
numberOfMembers: Long? = null,
|
numberOfMembers: Long? = null,
|
||||||
isDirect: Boolean = false,
|
isDirect: Boolean = false,
|
||||||
|
roomType: RoomType = RoomType.Room,
|
||||||
roomAvatarUrl: String? = null,
|
roomAvatarUrl: String? = null,
|
||||||
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
||||||
) = ContentState.Loaded(
|
) = ContentState.Loaded(
|
||||||
|
|
@ -88,6 +117,7 @@ fun aLoadedContentState(
|
||||||
topic = topic,
|
topic = topic,
|
||||||
numberOfMembers = numberOfMembers,
|
numberOfMembers = numberOfMembers,
|
||||||
isDirect = isDirect,
|
isDirect = isDirect,
|
||||||
|
roomType = roomType,
|
||||||
roomAvatarUrl = roomAvatarUrl,
|
roomAvatarUrl = roomAvatarUrl,
|
||||||
joinAuthorisationStatus = joinAuthorisationStatus
|
joinAuthorisationStatus = joinAuthorisationStatus
|
||||||
)
|
)
|
||||||
|
|
@ -95,12 +125,25 @@ fun aLoadedContentState(
|
||||||
fun aJoinRoomState(
|
fun aJoinRoomState(
|
||||||
contentState: ContentState = aLoadedContentState(),
|
contentState: ContentState = aLoadedContentState(),
|
||||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||||
|
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (JoinRoomEvents) -> Unit = {}
|
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||||
) = JoinRoomState(
|
) = JoinRoomState(
|
||||||
contentState = contentState,
|
contentState = contentState,
|
||||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||||
|
knockAction = knockAction,
|
||||||
|
applicationName = "AppName",
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun anInviteSender(
|
||||||
|
userId: UserId = UserId("@bob:domain"),
|
||||||
|
displayName: String = "Bob",
|
||||||
|
avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
|
||||||
|
) = InviteSender(
|
||||||
|
userId = userId,
|
||||||
|
displayName = displayName,
|
||||||
|
avatarData = avatarData,
|
||||||
|
)
|
||||||
|
|
||||||
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
||||||
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")
|
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,25 @@
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
||||||
|
|
@ -35,9 +44,12 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolec
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.background.LightGradientBackground
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.button.SuperButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
|
@ -46,40 +58,61 @@ import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun JoinRoomView(
|
fun JoinRoomView(
|
||||||
state: JoinRoomState,
|
state: JoinRoomState,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
|
onKnockSuccess: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
HeaderFooterPage(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier.fillMaxSize(),
|
||||||
paddingValues = PaddingValues(16.dp),
|
) {
|
||||||
topBar = {
|
LightGradientBackground()
|
||||||
JoinRoomTopBar(onBackClicked = onBackPressed)
|
HeaderFooterPage(
|
||||||
},
|
containerColor = Color.Transparent,
|
||||||
content = {
|
paddingValues = PaddingValues(16.dp),
|
||||||
JoinRoomContent(contentState = state.contentState)
|
topBar = {
|
||||||
},
|
JoinRoomTopBar(onBackClicked = onBackPressed)
|
||||||
footer = {
|
},
|
||||||
JoinRoomFooter(
|
content = {
|
||||||
state = state,
|
JoinRoomContent(
|
||||||
onAcceptInvite = {
|
contentState = state.contentState,
|
||||||
state.eventSink(JoinRoomEvents.AcceptInvite)
|
applicationName = state.applicationName,
|
||||||
},
|
)
|
||||||
onDeclineInvite = {
|
},
|
||||||
state.eventSink(JoinRoomEvents.DeclineInvite)
|
footer = {
|
||||||
},
|
JoinRoomFooter(
|
||||||
onJoinRoom = {
|
state = state,
|
||||||
state.eventSink(JoinRoomEvents.JoinRoom)
|
onAcceptInvite = {
|
||||||
},
|
state.eventSink(JoinRoomEvents.AcceptInvite)
|
||||||
onRetry = {
|
},
|
||||||
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
onDeclineInvite = {
|
||||||
}
|
state.eventSink(JoinRoomEvents.DeclineInvite)
|
||||||
)
|
},
|
||||||
}
|
onJoinRoom = {
|
||||||
|
state.eventSink(JoinRoomEvents.JoinRoom)
|
||||||
|
},
|
||||||
|
onKnockRoom = {
|
||||||
|
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||||
|
},
|
||||||
|
onRetry = {
|
||||||
|
state.eventSink(JoinRoomEvents.RetryFetchingContent)
|
||||||
|
},
|
||||||
|
onGoBack = onBackPressed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.knockAction,
|
||||||
|
onSuccess = { onKnockSuccess() },
|
||||||
|
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +122,9 @@ private fun JoinRoomFooter(
|
||||||
onAcceptInvite: () -> Unit,
|
onAcceptInvite: () -> Unit,
|
||||||
onDeclineInvite: () -> Unit,
|
onDeclineInvite: () -> Unit,
|
||||||
onJoinRoom: () -> Unit,
|
onJoinRoom: () -> Unit,
|
||||||
|
onKnockRoom: () -> Unit,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
|
onGoBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.contentState is ContentState.Failure) {
|
if (state.contentState is ContentState.Failure) {
|
||||||
|
|
@ -97,41 +132,51 @@ private fun JoinRoomFooter(
|
||||||
text = stringResource(CommonStrings.action_retry),
|
text = stringResource(CommonStrings.action_retry),
|
||||||
onClick = onRetry,
|
onClick = onRetry,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Large,
|
||||||
|
)
|
||||||
|
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_go_back),
|
||||||
|
onClick = onGoBack,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
size = ButtonSize.Large,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val joinAuthorisationStatus = state.joinAuthorisationStatus
|
val joinAuthorisationStatus = state.joinAuthorisationStatus
|
||||||
when (joinAuthorisationStatus) {
|
when (joinAuthorisationStatus) {
|
||||||
JoinAuthorisationStatus.IsInvited -> {
|
is JoinAuthorisationStatus.IsInvited -> {
|
||||||
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
text = stringResource(CommonStrings.action_decline),
|
text = stringResource(CommonStrings.action_decline),
|
||||||
onClick = onDeclineInvite,
|
onClick = onDeclineInvite,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Large,
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(CommonStrings.action_accept),
|
text = stringResource(CommonStrings.action_accept),
|
||||||
onClick = onAcceptInvite,
|
onClick = onAcceptInvite,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Large,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JoinAuthorisationStatus.CanJoin -> {
|
JoinAuthorisationStatus.CanJoin -> {
|
||||||
Button(
|
SuperButton(
|
||||||
text = stringResource(R.string.screen_join_room_join_action),
|
|
||||||
onClick = onJoinRoom,
|
onClick = onJoinRoom,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.Medium,
|
buttonSize = ButtonSize.Large,
|
||||||
)
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_join_room_join_action),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
JoinAuthorisationStatus.CanKnock -> {
|
JoinAuthorisationStatus.CanKnock -> {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(R.string.screen_join_room_knock_action),
|
text = stringResource(R.string.screen_join_room_knock_action),
|
||||||
onClick = onJoinRoom,
|
onClick = onKnockRoom,
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Large,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
JoinAuthorisationStatus.Unknown -> Unit
|
JoinAuthorisationStatus.Unknown -> Unit
|
||||||
|
|
@ -142,6 +187,7 @@ private fun JoinRoomFooter(
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinRoomContent(
|
private fun JoinRoomContent(
|
||||||
contentState: ContentState,
|
contentState: ContentState,
|
||||||
|
applicationName: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (contentState) {
|
when (contentState) {
|
||||||
|
|
@ -152,13 +198,48 @@ private fun JoinRoomContent(
|
||||||
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
RoomPreviewTitleAtom(contentState.computedTitle)
|
if (contentState.name != null) {
|
||||||
|
RoomPreviewTitleAtom(
|
||||||
|
title = contentState.name,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RoomPreviewTitleAtom(
|
||||||
|
title = stringResource(id = CommonStrings.common_no_room_name),
|
||||||
|
fontStyle = FontStyle.Italic
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
subtitle = {
|
subtitle = {
|
||||||
RoomPreviewSubtitleAtom(contentState.computedSubtitle)
|
if (contentState.alias != null) {
|
||||||
|
RoomPreviewSubtitleAtom(contentState.alias.value)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description = {
|
description = {
|
||||||
RoomPreviewDescriptionAtom(contentState.topic ?: "")
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
|
||||||
|
if (inviteSender != null) {
|
||||||
|
InviteSenderView(inviteSender = inviteSender)
|
||||||
|
}
|
||||||
|
RoomPreviewDescriptionAtom(contentState.topic ?: "")
|
||||||
|
if (contentState.roomType == RoomType.Space) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_join_room_space_not_supported_title),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_join_room_space_not_supported_description, applicationName),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
memberCount = {
|
memberCount = {
|
||||||
if (contentState.showMemberCount) {
|
if (contentState.showMemberCount) {
|
||||||
|
|
@ -213,7 +294,7 @@ private fun JoinRoomContent(
|
||||||
},
|
},
|
||||||
subtitle = {
|
subtitle = {
|
||||||
Text(
|
Text(
|
||||||
text = "Failed to get information about the room",
|
text = stringResource(id = CommonStrings.error_unknown),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
)
|
)
|
||||||
|
|
@ -241,6 +322,7 @@ private fun JoinRoomTopBar(
|
||||||
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
|
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
|
||||||
JoinRoomView(
|
JoinRoomView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackPressed = { }
|
onBackPressed = { },
|
||||||
|
onKnockSuccess = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.joinroom.impl.JoinRoomPresenter
|
import io.element.android.features.joinroom.impl.JoinRoomPresenter
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -35,7 +36,9 @@ object JoinRoomModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun providesJoinRoomPresenterFactory(
|
fun providesJoinRoomPresenterFactory(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
|
knockRoom: KnockRoom,
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||||
|
buildMeta: BuildMeta,
|
||||||
): JoinRoomPresenter.Factory {
|
): JoinRoomPresenter.Factory {
|
||||||
return object : JoinRoomPresenter.Factory {
|
return object : JoinRoomPresenter.Factory {
|
||||||
override fun create(
|
override fun create(
|
||||||
|
|
@ -48,7 +51,9 @@ object JoinRoomModule {
|
||||||
roomIdOrAlias = roomIdOrAlias,
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
roomDescription = roomDescription,
|
roomDescription = roomDescription,
|
||||||
matrixClient = client,
|
matrixClient = client,
|
||||||
|
knockRoom = knockRoom,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
|
buildMeta = buildMeta,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.joinroom.impl.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface KnockRoom {
|
||||||
|
suspend operator fun invoke(roomId: RoomId): Result<Unit>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
|
||||||
|
override suspend fun invoke(roomId: RoomId) = client.knockRoom(roomId)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_join_room_join_action">"Join room"</string>
|
<string name="screen_join_room_join_action">"Join room"</string>
|
||||||
<string name="screen_join_room_knock_action">"Knock to join"</string>
|
<string name="screen_join_room_knock_action">"Knock to join"</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_description">"%1$s does not support spaces yet. You can access spaces on web."</string>
|
||||||
|
<string name="screen_join_room_space_not_supported_title">"Spaces are not supported yet"</string>
|
||||||
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."</string>
|
<string name="screen_join_room_subtitle_knock">"Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved."</string>
|
||||||
<string name="screen_join_room_subtitle_no_preview">"You must be a member of this room to view the message history."</string>
|
<string name="screen_join_room_subtitle_no_preview">"You must be a member of this room to view the message history."</string>
|
||||||
<string name="screen_join_room_title_knock">"Want to join this room?"</string>
|
<string name="screen_join_room_title_knock">"Want to join this room?"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.joinroom.impl
|
||||||
|
|
||||||
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
class FakeKnockRoom(
|
||||||
|
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
|
||||||
|
) : KnockRoom {
|
||||||
|
override suspend fun invoke(roomId: RoomId) = lambda(roomId)
|
||||||
|
}
|
||||||
|
|
@ -20,19 +20,27 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||||
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -56,6 +64,7 @@ class JoinRoomPresenterTest {
|
||||||
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
|
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
|
||||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||||
|
assertThat(state.applicationName).isEqualTo("AppName")
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +110,31 @@ class JoinRoomPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited)
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
|
||||||
|
val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob")
|
||||||
|
val expectedInviteSender = inviter.toInviteSender()
|
||||||
|
val roomInfo = aRoomInfo(
|
||||||
|
currentUserMembership = CurrentUserMembership.INVITED,
|
||||||
|
inviter = inviter,
|
||||||
|
)
|
||||||
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
|
getRoomInfoFlowLambda = { _ ->
|
||||||
|
flowOf(Optional.of(roomInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = matrixClient
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(expectedInviteSender))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,6 +275,38 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - emit knock room event`() = runTest {
|
||||||
|
val knockRoomSuccess = lambdaRecorder { _: RoomId ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
val knockRoomFailure = lambdaRecorder { roomId: RoomId ->
|
||||||
|
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId"))
|
||||||
|
}
|
||||||
|
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
|
||||||
|
val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
|
fakeKnockRoom.lambda = knockRoomFailure
|
||||||
|
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.knockAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(knockRoomSuccess)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID))
|
||||||
|
assert(knockRoomFailure)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room is not known RoomPreview is loaded`() = runTest {
|
fun `present - when room is not known RoomPreview is loaded`() = runTest {
|
||||||
val client = FakeMatrixClient(
|
val client = FakeMatrixClient(
|
||||||
|
|
@ -254,7 +319,7 @@ class JoinRoomPresenterTest {
|
||||||
topic = "Room topic",
|
topic = "Room topic",
|
||||||
avatarUrl = "avatarUrl",
|
avatarUrl = "avatarUrl",
|
||||||
numberOfJoinedMembers = 2,
|
numberOfJoinedMembers = 2,
|
||||||
roomType = null,
|
roomType = RoomType.Room,
|
||||||
isHistoryWorldReadable = false,
|
isHistoryWorldReadable = false,
|
||||||
isJoined = false,
|
isJoined = false,
|
||||||
isInvited = false,
|
isInvited = false,
|
||||||
|
|
@ -278,6 +343,7 @@ class JoinRoomPresenterTest {
|
||||||
alias = RoomAlias("#alias:matrix.org"),
|
alias = RoomAlias("#alias:matrix.org"),
|
||||||
numberOfMembers = 2,
|
numberOfMembers = 2,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
|
roomType = RoomType.Room,
|
||||||
roomAvatarUrl = "avatarUrl",
|
roomAvatarUrl = "avatarUrl",
|
||||||
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
|
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
|
||||||
)
|
)
|
||||||
|
|
@ -350,6 +416,8 @@ class JoinRoomPresenterTest {
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
roomDescription: Optional<RoomDescription> = Optional.empty(),
|
roomDescription: Optional<RoomDescription> = Optional.empty(),
|
||||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||||
|
knockRoom: KnockRoom = FakeKnockRoom(),
|
||||||
|
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
|
||||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
|
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
|
||||||
): JoinRoomPresenter {
|
): JoinRoomPresenter {
|
||||||
return JoinRoomPresenter(
|
return JoinRoomPresenter(
|
||||||
|
|
@ -357,6 +425,8 @@ class JoinRoomPresenterTest {
|
||||||
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||||
roomDescription = roomDescription,
|
roomDescription = roomDescription,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
|
knockRoom = knockRoom,
|
||||||
|
buildMeta = buildMeta,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* 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.joinroom.impl
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class JoinRoomViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invoke the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackPressed = it
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Join room on CanJoin room emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_join_room_join_action)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Knock room on CanKnock room emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_join_room_knock_action)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on closing Knock error emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
|
||||||
|
knockAction = AsyncAction.Failure(Exception("Error")),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Accept invitation IsInvited room emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_accept)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Decline invitation on IsInvited room emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Retry when an error occurs emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aFailureContentState(),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_retry)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Go back when a space is displayed invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setJoinRoomView(
|
||||||
|
aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(roomType = RoomType.Space),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackPressed = it
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_go_back)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
|
||||||
|
state: JoinRoomState,
|
||||||
|
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
JoinRoomView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
onKnockSuccess = onKnockSuccess,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -358,7 +358,7 @@ private fun PinUnlockFooter(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
internal fun PinUnlockInAppViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
PinUnlockView(
|
PinUnlockView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
@ -369,7 +369,7 @@ internal fun PinUnlockInAppViewPreview(@PreviewParameter(PinUnlockStateProvider:
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
internal fun PinUnlockDefaultViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
PinUnlockView(
|
PinUnlockView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string>
|
<string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Är du säker på att du vill ta bort PIN-koden?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_message">"Är du säker på att du vill ta bort PIN-koden?"</string>
|
||||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string>
|
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string>
|
||||||
|
<string name="screen_app_lock_signout_alert_message">"Du måste logga in igen och skapa en ny PIN-kod för att fortsätta"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"Du blir utloggad"</string>
|
<string name="screen_app_lock_signout_alert_title">"Du blir utloggad"</string>
|
||||||
<plurals name="screen_app_lock_subtitle">
|
<plurals name="screen_app_lock_subtitle">
|
||||||
<item quantity="one">"Du har %1$d försök att låsa upp"</item>
|
<item quantity="one">"Du har %1$d försök att låsa upp"</item>
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,11 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
),
|
),
|
||||||
anActionListState().copy(
|
anActionListState().copy(
|
||||||
target = ActionListState.Target.Success(
|
target = ActionListState.Target.Success(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemImageContent()).copy(
|
event = aTimelineItemEvent(
|
||||||
reactionsState = reactionsState
|
content = aTimelineItemImageContent(),
|
||||||
|
displayNameAmbiguous = true,
|
||||||
|
).copy(
|
||||||
|
reactionsState = reactionsState,
|
||||||
),
|
),
|
||||||
displayEmojiReactions = true,
|
displayEmojiReactions = true,
|
||||||
actions = aTimelineItemActionList(),
|
actions = aTimelineItemActionList(),
|
||||||
|
|
@ -142,6 +145,7 @@ fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
|
||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
|
||||||
return persistentListOf(
|
return persistentListOf(
|
||||||
TimelineItemAction.EndPoll,
|
TimelineItemAction.EndPoll,
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
|
import io.element.android.features.messages.impl.sender.SenderName
|
||||||
|
import io.element.android.features.messages.impl.sender.SenderNameMode
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||||
|
|
@ -268,15 +270,11 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||||
icon()
|
icon()
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Row {
|
SenderName(
|
||||||
if (event.senderDisplayName != null) {
|
senderId = event.senderId,
|
||||||
Text(
|
senderProfile = event.senderProfile,
|
||||||
text = event.senderDisplayName,
|
senderNameMode = SenderNameMode.ActionList,
|
||||||
style = ElementTheme.typography.fontBodySmMedium,
|
)
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ private fun RoomMemberSuggestionItemView(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MentionSuggestionsPickerView_Preview() {
|
internal fun MentionSuggestionsPickerViewPreview() {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
val roomMember = RoomMember(
|
val roomMember = RoomMember(
|
||||||
userId = UserId("@alice:server.org"),
|
userId = UserId("@alice:server.org"),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* 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.sender
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
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.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
|
|
||||||
|
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
|
||||||
|
@Composable
|
||||||
|
fun SenderName(
|
||||||
|
senderId: UserId,
|
||||||
|
senderProfile: ProfileTimelineDetails,
|
||||||
|
senderNameMode: SenderNameMode,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
when (senderProfile) {
|
||||||
|
is ProfileTimelineDetails.Error,
|
||||||
|
ProfileTimelineDetails.Pending,
|
||||||
|
ProfileTimelineDetails.Unavailable -> {
|
||||||
|
MainText(text = senderId.value, mode = senderNameMode)
|
||||||
|
}
|
||||||
|
is ProfileTimelineDetails.Ready -> {
|
||||||
|
val displayName = senderProfile.displayName
|
||||||
|
if (displayName.isNullOrEmpty()) {
|
||||||
|
MainText(text = senderId.value, mode = senderNameMode)
|
||||||
|
} else {
|
||||||
|
MainText(text = displayName, mode = senderNameMode)
|
||||||
|
if (senderProfile.displayNameAmbiguous) {
|
||||||
|
SecondaryText(text = senderId.value, mode = senderNameMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.MainText(
|
||||||
|
text: String,
|
||||||
|
mode: SenderNameMode,
|
||||||
|
) {
|
||||||
|
val style = when (mode) {
|
||||||
|
is SenderNameMode.Timeline -> ElementTheme.typography.fontBodyMdMedium
|
||||||
|
SenderNameMode.ActionList,
|
||||||
|
SenderNameMode.Reply -> ElementTheme.typography.fontBodySmMedium
|
||||||
|
}
|
||||||
|
val modifier = when (mode) {
|
||||||
|
is SenderNameMode.Timeline -> Modifier.alignByBaseline()
|
||||||
|
SenderNameMode.ActionList,
|
||||||
|
SenderNameMode.Reply -> Modifier
|
||||||
|
}
|
||||||
|
val color = when (mode) {
|
||||||
|
is SenderNameMode.Timeline -> mode.mainColor
|
||||||
|
SenderNameMode.ActionList,
|
||||||
|
SenderNameMode.Reply -> MaterialTheme.colorScheme.primary
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = modifier.clipToBounds(),
|
||||||
|
text = text,
|
||||||
|
style = style,
|
||||||
|
color = color,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.SecondaryText(
|
||||||
|
text: String,
|
||||||
|
mode: SenderNameMode,
|
||||||
|
) {
|
||||||
|
val style = when (mode) {
|
||||||
|
is SenderNameMode.Timeline -> ElementTheme.typography.fontBodySmRegular
|
||||||
|
SenderNameMode.ActionList,
|
||||||
|
SenderNameMode.Reply -> ElementTheme.typography.fontBodyXsRegular
|
||||||
|
}
|
||||||
|
val modifier = when (mode) {
|
||||||
|
is SenderNameMode.Timeline -> Modifier.alignByBaseline()
|
||||||
|
SenderNameMode.ActionList,
|
||||||
|
SenderNameMode.Reply -> Modifier
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = modifier.clipToBounds(),
|
||||||
|
text = text,
|
||||||
|
style = style,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun SenderNamePreview(
|
||||||
|
@PreviewParameter(SenderNameDataProvider::class) senderNameData: SenderNameData,
|
||||||
|
) = ElementPreview {
|
||||||
|
SenderName(
|
||||||
|
senderId = senderNameData.userId,
|
||||||
|
senderProfile = senderNameData.profileTimelineDetails,
|
||||||
|
senderNameMode = senderNameData.senderNameMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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.sender
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
|
|
||||||
|
data class SenderNameData(
|
||||||
|
val userId: UserId,
|
||||||
|
val profileTimelineDetails: ProfileTimelineDetails,
|
||||||
|
val senderNameMode: SenderNameMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
open class SenderNameDataProvider : PreviewParameterProvider<SenderNameData> {
|
||||||
|
override val values: Sequence<SenderNameData>
|
||||||
|
get() = sequenceOf(
|
||||||
|
SenderNameMode.Timeline(mainColor = Color.Red),
|
||||||
|
SenderNameMode.Reply,
|
||||||
|
SenderNameMode.ActionList,
|
||||||
|
)
|
||||||
|
.flatMap { senderNameMode ->
|
||||||
|
sequenceOf(
|
||||||
|
aSenderNameData(
|
||||||
|
senderNameMode = senderNameMode,
|
||||||
|
),
|
||||||
|
aSenderNameData(
|
||||||
|
senderNameMode = senderNameMode,
|
||||||
|
displayNameAmbiguous = true,
|
||||||
|
),
|
||||||
|
SenderNameData(
|
||||||
|
senderNameMode = senderNameMode,
|
||||||
|
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
|
||||||
|
profileTimelineDetails = ProfileTimelineDetails.Unavailable,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aSenderNameData(
|
||||||
|
senderNameMode: SenderNameMode,
|
||||||
|
displayNameAmbiguous: Boolean = false,
|
||||||
|
) = SenderNameData(
|
||||||
|
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
|
||||||
|
profileTimelineDetails = aProfileTimelineDetailsReady(
|
||||||
|
displayName = "Alice ${senderNameMode.javaClass.simpleName}",
|
||||||
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
|
),
|
||||||
|
senderNameMode = senderNameMode,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.sender
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
sealed interface SenderNameMode {
|
||||||
|
data class Timeline(val mainColor: Color) : SenderNameMode
|
||||||
|
data object Reply : SenderNameMode
|
||||||
|
data object ActionList : SenderNameMode
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
|
|
@ -130,6 +131,7 @@ internal fun aTimelineItemEvent(
|
||||||
isMine: Boolean = false,
|
isMine: Boolean = false,
|
||||||
isEditable: Boolean = false,
|
isEditable: Boolean = false,
|
||||||
senderDisplayName: String = "Sender",
|
senderDisplayName: String = "Sender",
|
||||||
|
displayNameAmbiguous: Boolean = false,
|
||||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||||
sendState: LocalEventSendState? = null,
|
sendState: LocalEventSendState? = null,
|
||||||
|
|
@ -151,7 +153,10 @@ internal fun aTimelineItemEvent(
|
||||||
sentTime = "12:34",
|
sentTime = "12:34",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
isEditable = isEditable,
|
isEditable = isEditable,
|
||||||
senderDisplayName = senderDisplayName,
|
senderProfile = aProfileTimelineDetailsReady(
|
||||||
|
displayName = senderDisplayName,
|
||||||
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
|
),
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
localSendState = sendState,
|
localSendState = sendState,
|
||||||
inReplyTo = inReplyTo,
|
inReplyTo = inReplyTo,
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ internal fun MessagesReactionButtonPreview(@PreviewParameter(AggregatedReactionP
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MessagesAddReactionButtonPreview() = ElementPreview {
|
internal fun MessagesReactionButtonAddPreview() = ElementPreview {
|
||||||
MessagesReactionButton(
|
MessagesReactionButton(
|
||||||
content = MessagesReactionsButtonContent.Icon(CompoundDrawables.ic_compound_reaction_add),
|
content = MessagesReactionsButtonContent.Icon(CompoundDrawables.ic_compound_reaction_add),
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -201,7 +201,7 @@ internal fun MessagesAddReactionButtonPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MessagesReactionExtraButtonsPreview() = ElementPreview {
|
internal fun MessagesReactionButtonExtraPreview() = ElementPreview {
|
||||||
Row {
|
Row {
|
||||||
MessagesReactionButton(
|
MessagesReactionButton(
|
||||||
content = MessagesReactionsButtonContent.Text("12 more"),
|
content = MessagesReactionsButtonContent.Text("12 more"),
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ import androidx.constraintlayout.compose.ConstrainScope
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.messages.impl.sender.SenderName
|
||||||
|
import io.element.android.features.messages.impl.sender.SenderNameMode
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
|
@ -106,6 +108,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -291,7 +295,8 @@ private fun TimelineItemEventRowContent(
|
||||||
val avatarStrokeSize = 3.dp
|
val avatarStrokeSize = 3.dp
|
||||||
if (event.showSenderInformation && !timelineRoomInfo.isDm) {
|
if (event.showSenderInformation && !timelineRoomInfo.isDm) {
|
||||||
MessageSenderInformation(
|
MessageSenderInformation(
|
||||||
event.safeSenderName,
|
event.senderId,
|
||||||
|
event.senderProfile,
|
||||||
event.senderAvatar,
|
event.senderAvatar,
|
||||||
avatarStrokeSize,
|
avatarStrokeSize,
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -371,7 +376,8 @@ private fun TimelineItemEventRowContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageSenderInformation(
|
private fun MessageSenderInformation(
|
||||||
sender: String,
|
senderId: UserId,
|
||||||
|
senderProfile: ProfileTimelineDetails,
|
||||||
senderAvatar: AvatarData,
|
senderAvatar: AvatarData,
|
||||||
avatarStrokeSize: Dp,
|
avatarStrokeSize: Dp,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|
@ -398,13 +404,10 @@ private fun MessageSenderInformation(
|
||||||
Row {
|
Row {
|
||||||
Avatar(senderAvatar)
|
Avatar(senderAvatar)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
SenderName(
|
||||||
modifier = Modifier.clipToBounds(),
|
senderId = senderId,
|
||||||
text = sender,
|
senderProfile = senderProfile,
|
||||||
maxLines = 1,
|
senderNameMode = SenderNameMode.Timeline(avatarColors.foreground),
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = avatarColors.foreground,
|
|
||||||
style = ElementTheme.typography.fontBodyMdMedium,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -561,10 +564,10 @@ private fun MessageEventBubbleContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
||||||
val senderName = inReplyTo.senderDisplayName ?: inReplyTo.senderId.value
|
|
||||||
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
||||||
ReplyToContent(
|
ReplyToContent(
|
||||||
senderName = senderName,
|
senderId = inReplyTo.senderId,
|
||||||
|
senderProfile = inReplyTo.senderProfile,
|
||||||
metadata = inReplyTo.metadata(),
|
metadata = inReplyTo.metadata(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||||
|
|
@ -609,7 +612,8 @@ private fun MessageEventBubbleContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReplyToContent(
|
private fun ReplyToContent(
|
||||||
senderName: String,
|
senderId: UserId,
|
||||||
|
senderProfile: ProfileTimelineDetails,
|
||||||
metadata: InReplyToMetadata?,
|
metadata: InReplyToMetadata?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -633,18 +637,15 @@ private fun ReplyToContent(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
val a11InReplyToText = stringResource(CommonStrings.common_in_reply_to, senderName)
|
val a11InReplyToText = stringResource(CommonStrings.common_in_reply_to, senderProfile.getDisambiguatedDisplayName(senderId))
|
||||||
Column(verticalArrangement = Arrangement.SpaceBetween) {
|
Column(verticalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text(
|
SenderName(
|
||||||
|
senderId = senderId,
|
||||||
|
senderProfile = senderProfile,
|
||||||
|
senderNameMode = SenderNameMode.Reply,
|
||||||
modifier = Modifier.semantics {
|
modifier = Modifier.semantics {
|
||||||
contentDescription = a11InReplyToText
|
contentDescription = a11InReplyToText
|
||||||
},
|
},
|
||||||
text = senderName,
|
|
||||||
style = ElementTheme.typography.fontBodySmMedium,
|
|
||||||
textAlign = TextAlign.Start,
|
|
||||||
color = ElementTheme.materialColors.primary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
)
|
||||||
ReplyToContentText(metadata)
|
ReplyToContentText(metadata)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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.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.TextMessageType
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemEventRowDisambiguatedPreview(
|
||||||
|
@PreviewParameter(InReplyToDetailsDisambiguatedProvider::class) inReplyToDetails: InReplyToDetails,
|
||||||
|
) = ElementPreview {
|
||||||
|
TimelineItemEventRowWithReplyContentToPreview(
|
||||||
|
inReplyToDetails = inReplyToDetails,
|
||||||
|
displayNameAmbiguous = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() {
|
||||||
|
override val values: Sequence<InReplyToDetails>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aMessageContent(
|
||||||
|
body = "Message which are being replied.",
|
||||||
|
type = TextMessageType("Message which are being replied.", null)
|
||||||
|
),
|
||||||
|
).map {
|
||||||
|
aInReplyToDetails(
|
||||||
|
displayNameAmbiguous = true,
|
||||||
|
eventContent = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,7 +43,6 @@ internal fun TimelineItemEventRowTimestampPreview(
|
||||||
body = str,
|
body = str,
|
||||||
),
|
),
|
||||||
reactionsState = aTimelineItemReactions(count = 0),
|
reactionsState = aTimelineItemReactions(count = 0),
|
||||||
senderDisplayName = "A sender",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageConten
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||||
|
|
@ -58,7 +59,10 @@ internal fun TimelineItemEventRowWithReplyPreview(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InReplyToDetails) {
|
internal fun TimelineItemEventRowWithReplyContentToPreview(
|
||||||
|
inReplyToDetails: InReplyToDetails,
|
||||||
|
displayNameAmbiguous: Boolean = false,
|
||||||
|
) {
|
||||||
Column {
|
Column {
|
||||||
sequenceOf(false, true).forEach {
|
sequenceOf(false, true).forEach {
|
||||||
ATimelineItemEventRow(
|
ATimelineItemEventRow(
|
||||||
|
|
@ -69,6 +73,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InR
|
||||||
body = "A reply."
|
body = "A reply."
|
||||||
),
|
),
|
||||||
inReplyTo = inReplyToDetails,
|
inReplyTo = inReplyToDetails,
|
||||||
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
groupPosition = TimelineItemGroupPosition.First,
|
groupPosition = TimelineItemGroupPosition.First,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -80,6 +85,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InR
|
||||||
aspectRatio = 2.5f
|
aspectRatio = 2.5f
|
||||||
),
|
),
|
||||||
inReplyTo = inReplyToDetails,
|
inReplyTo = inReplyToDetails,
|
||||||
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
isThreaded = true,
|
isThreaded = true,
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
),
|
),
|
||||||
|
|
@ -150,7 +156,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aMessageContent(
|
protected fun aMessageContent(
|
||||||
body: String,
|
body: String,
|
||||||
type: MessageType,
|
type: MessageType,
|
||||||
) = MessageContent(
|
) = MessageContent(
|
||||||
|
|
@ -163,12 +169,24 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
||||||
|
|
||||||
protected fun aInReplyToDetails(
|
protected fun aInReplyToDetails(
|
||||||
eventContent: EventContent,
|
eventContent: EventContent,
|
||||||
|
displayNameAmbiguous: Boolean = false,
|
||||||
) = InReplyToDetails(
|
) = InReplyToDetails(
|
||||||
eventId = EventId("\$event"),
|
eventId = EventId("\$event"),
|
||||||
eventContent = eventContent,
|
eventContent = eventContent,
|
||||||
senderId = UserId("@Sender:domain"),
|
senderId = UserId("@Sender:domain"),
|
||||||
senderDisplayName = "Sender",
|
senderProfile = aProfileTimelineDetailsReady(
|
||||||
senderAvatarUrl = null,
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
|
),
|
||||||
textContent = (eventContent as? MessageContent)?.body.orEmpty(),
|
textContent = (eventContent as? MessageContent)?.body.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun aProfileTimelineDetailsReady(
|
||||||
|
displayName: String? = "Sender",
|
||||||
|
displayNameAmbiguous: Boolean = false,
|
||||||
|
avatarUrl: String? = null,
|
||||||
|
) = ProfileTimelineDetails.Ready(
|
||||||
|
displayName = displayName,
|
||||||
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ private fun computeReceiptDescription(receipts: ImmutableList<ReadReceiptData>):
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemReactionsViewPreview(
|
internal fun TimelineItemReadReceiptViewPreview(
|
||||||
@PreviewParameter(ReadReceiptViewStateProvider::class) state: ReadReceiptViewState,
|
@PreviewParameter(ReadReceiptViewStateProvider::class) state: ReadReceiptViewState,
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
TimelineItemReadReceiptView(
|
TimelineItemReadReceiptView(
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,12 @@ class TimelineItemContentFactory @Inject constructor(
|
||||||
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
||||||
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
||||||
is MessageContent -> {
|
is MessageContent -> {
|
||||||
val senderDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender)
|
val senderDisambiguatedDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender)
|
||||||
messageFactory.create(itemContent, senderDisplayName, eventTimelineItem.eventId)
|
messageFactory.create(
|
||||||
|
content = itemContent,
|
||||||
|
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||||
|
eventId = eventTimelineItem.eventId,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
|
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
|
||||||
is RedactedContent -> redactedMessageFactory.create(itemContent)
|
is RedactedContent -> redactedMessageFactory.create(itemContent)
|
||||||
|
|
|
||||||
|
|
@ -70,17 +70,21 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||||
private val htmlConverterProvider: HtmlConverterProvider,
|
private val htmlConverterProvider: HtmlConverterProvider,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
) {
|
) {
|
||||||
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
|
suspend fun create(
|
||||||
|
content: MessageContent,
|
||||||
|
senderDisambiguatedDisplayName: String,
|
||||||
|
eventId: EventId?,
|
||||||
|
): TimelineItemEventContent {
|
||||||
return when (val messageType = content.type) {
|
return when (val messageType = content.type) {
|
||||||
is EmoteMessageType -> {
|
is EmoteMessageType -> {
|
||||||
val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}"
|
val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}"
|
||||||
TimelineItemEmoteContent(
|
TimelineItemEmoteContent(
|
||||||
body = emoteBody,
|
body = emoteBody,
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(
|
htmlDocument = messageType.formatted?.toHtmlDocument(
|
||||||
permalinkParser = permalinkParser,
|
permalinkParser = permalinkParser,
|
||||||
prefix = "* $senderDisplayName",
|
prefix = "* $senderDisambiguatedDisplayName",
|
||||||
),
|
),
|
||||||
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(),
|
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisambiguatedDisplayName") ?: emoteBody.withLinks(),
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
|
@ -55,15 +55,14 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
val currentSender = currentTimelineItem.event.sender
|
val currentSender = currentTimelineItem.event.sender
|
||||||
val groupPosition =
|
val groupPosition =
|
||||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||||
val (senderDisplayName, senderAvatarUrl) = currentTimelineItem.getSenderInfo()
|
val senderProfile = currentTimelineItem.event.senderProfile
|
||||||
|
|
||||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||||
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
|
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
|
||||||
|
|
||||||
val senderAvatarData = AvatarData(
|
val senderAvatarData = AvatarData(
|
||||||
id = currentSender.value,
|
id = currentSender.value,
|
||||||
name = senderDisplayName ?: currentSender.value,
|
name = senderProfile.getDisambiguatedDisplayName(currentSender),
|
||||||
url = senderAvatarUrl,
|
url = senderProfile.getAvatarUrl(),
|
||||||
size = AvatarSize.TimelineSender
|
size = AvatarSize.TimelineSender
|
||||||
)
|
)
|
||||||
currentTimelineItem.event
|
currentTimelineItem.event
|
||||||
|
|
@ -72,7 +71,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
eventId = currentTimelineItem.eventId,
|
eventId = currentTimelineItem.eventId,
|
||||||
transactionId = currentTimelineItem.transactionId,
|
transactionId = currentTimelineItem.transactionId,
|
||||||
senderId = currentSender,
|
senderId = currentSender,
|
||||||
senderDisplayName = senderDisplayName,
|
senderProfile = senderProfile,
|
||||||
senderAvatar = senderAvatarData,
|
senderAvatar = senderAvatarData,
|
||||||
content = contentFactory.create(currentTimelineItem.event),
|
content = contentFactory.create(currentTimelineItem.event),
|
||||||
isMine = currentTimelineItem.event.isOwn,
|
isMine = currentTimelineItem.event.isOwn,
|
||||||
|
|
@ -99,26 +98,6 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MatrixTimelineItem.Event.getSenderInfo(): Pair<String?, String?> {
|
|
||||||
val senderDisplayName: String?
|
|
||||||
val senderAvatarUrl: String?
|
|
||||||
|
|
||||||
when (val senderProfile = event.senderProfile) {
|
|
||||||
ProfileTimelineDetails.Unavailable,
|
|
||||||
ProfileTimelineDetails.Pending,
|
|
||||||
is ProfileTimelineDetails.Error -> {
|
|
||||||
senderDisplayName = null
|
|
||||||
senderAvatarUrl = null
|
|
||||||
}
|
|
||||||
is ProfileTimelineDetails.Ready -> {
|
|
||||||
senderDisplayName = senderProfile.getDisambiguatedDisplayName(event.sender)
|
|
||||||
senderAvatarUrl = senderProfile.avatarUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return senderDisplayName to senderAvatarUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
||||||
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||||
var aggregatedReactions = event.reactions.map { reaction ->
|
var aggregatedReactions = event.reactions.map { reaction ->
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
import io.element.android.libraries.matrix.ui.messages.toPlainText
|
import io.element.android.libraries.matrix.ui.messages.toPlainText
|
||||||
|
|
@ -29,8 +30,7 @@ import io.element.android.libraries.matrix.ui.messages.toPlainText
|
||||||
data class InReplyToDetails(
|
data class InReplyToDetails(
|
||||||
val eventId: EventId,
|
val eventId: EventId,
|
||||||
val senderId: UserId,
|
val senderId: UserId,
|
||||||
val senderDisplayName: String?,
|
val senderProfile: ProfileTimelineDetails,
|
||||||
val senderAvatarUrl: String?,
|
|
||||||
val eventContent: EventContent?,
|
val eventContent: EventContent?,
|
||||||
val textContent: String?,
|
val textContent: String?,
|
||||||
)
|
)
|
||||||
|
|
@ -41,8 +41,7 @@ fun InReplyTo.map(
|
||||||
is InReplyTo.Ready -> InReplyToDetails(
|
is InReplyTo.Ready -> InReplyToDetails(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = senderId,
|
senderId = senderId,
|
||||||
senderDisplayName = senderDisplayName,
|
senderProfile = senderProfile,
|
||||||
senderAvatarUrl = senderAvatarUrl,
|
|
||||||
eventContent = content,
|
eventContent = content,
|
||||||
textContent = when (content) {
|
textContent = when (content) {
|
||||||
is MessageContent -> {
|
is MessageContent -> {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ import io.element.android.libraries.matrix.api.core.TransactionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -65,7 +67,7 @@ sealed interface TimelineItem {
|
||||||
val eventId: EventId? = null,
|
val eventId: EventId? = null,
|
||||||
val transactionId: TransactionId? = null,
|
val transactionId: TransactionId? = null,
|
||||||
val senderId: UserId,
|
val senderId: UserId,
|
||||||
val senderDisplayName: String?,
|
val senderProfile: ProfileTimelineDetails,
|
||||||
val senderAvatar: AvatarData,
|
val senderAvatar: AvatarData,
|
||||||
val content: TimelineItemEventContent,
|
val content: TimelineItemEventContent,
|
||||||
val sentTime: String = "",
|
val sentTime: String = "",
|
||||||
|
|
@ -82,7 +84,7 @@ sealed interface TimelineItem {
|
||||||
) : TimelineItem {
|
) : TimelineItem {
|
||||||
val showSenderInformation = groupPosition.isNew() && !isMine
|
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||||
|
|
||||||
val safeSenderName: String = senderDisplayName ?: senderId.value
|
val safeSenderName: String = senderProfile.getDisambiguatedDisplayName(senderId)
|
||||||
|
|
||||||
val failedToSend: Boolean = localSendState is LocalEventSendState.SendingFailed
|
val failedToSend: Boolean = localSendState is LocalEventSendState.SendingFailed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.fixtures
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
|
import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady
|
||||||
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
||||||
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
@ -48,7 +49,7 @@ internal fun aMessageEvent(
|
||||||
id = eventId?.value.orEmpty(),
|
id = eventId?.value.orEmpty(),
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = A_USER_ID,
|
senderId = A_USER_ID,
|
||||||
senderDisplayName = A_USER_NAME,
|
senderProfile = aProfileTimelineDetailsReady(displayName = A_USER_NAME),
|
||||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender),
|
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender),
|
||||||
content = content,
|
content = content,
|
||||||
sentTime = "",
|
sentTime = "",
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = OtherMessageType(msgType = "a_type", body = "body")),
|
content = createMessageContent(type = OtherMessageType(msgType = "a_type", body = "body")),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemTextContent(
|
val expected = TimelineItemTextContent(
|
||||||
|
|
@ -100,7 +100,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description")),
|
content = createMessageContent(type = LocationMessageType("body", "geo:1,2", "description")),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemLocationContent(
|
val expected = TimelineItemLocationContent(
|
||||||
|
|
@ -116,7 +116,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = LocationMessageType("body", "", null)),
|
content = createMessageContent(type = LocationMessageType("body", "", null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemTextContent(
|
val expected = TimelineItemTextContent(
|
||||||
|
|
@ -134,7 +134,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = TextMessageType("body", null)),
|
content = createMessageContent(type = TextMessageType("body", null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemTextContent(
|
val expected = TimelineItemTextContent(
|
||||||
|
|
@ -152,7 +152,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
|
content = createMessageContent(type = TextMessageType("https://www.example.org", null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
) as TimelineItemTextContent
|
) as TimelineItemTextContent
|
||||||
val expected = TimelineItemTextContent(
|
val expected = TimelineItemTextContent(
|
||||||
|
|
@ -200,7 +200,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
formatted = FormattedBody(MessageFormat.HTML, expected.toString())
|
formatted = FormattedBody(MessageFormat.HTML, expected.toString())
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expected)
|
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expected)
|
||||||
|
|
@ -218,7 +218,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
|
formatted = FormattedBody(MessageFormat.UNKNOWN, "formatted")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
assertThat((result as TimelineItemTextContent).formattedBody).isNull()
|
assertThat((result as TimelineItemTextContent).formattedBody).isNull()
|
||||||
|
|
@ -229,7 +229,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = VideoMessageType("body", null, null, MediaSource("url"), null)),
|
content = createMessageContent(type = VideoMessageType("body", null, null, MediaSource("url"), null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemVideoContent(
|
val expected = TimelineItemVideoContent(
|
||||||
|
|
@ -277,7 +277,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemVideoContent(
|
val expected = TimelineItemVideoContent(
|
||||||
|
|
@ -303,7 +303,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = AudioMessageType("body", MediaSource("url"), null)),
|
content = createMessageContent(type = AudioMessageType("body", MediaSource("url"), null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemAudioContent(
|
val expected = TimelineItemAudioContent(
|
||||||
|
|
@ -332,7 +332,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemAudioContent(
|
val expected = TimelineItemAudioContent(
|
||||||
|
|
@ -351,7 +351,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)),
|
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemVoiceContent(
|
val expected = TimelineItemVoiceContent(
|
||||||
|
|
@ -384,7 +384,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemVoiceContent(
|
val expected = TimelineItemVoiceContent(
|
||||||
|
|
@ -409,7 +409,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
)
|
)
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)),
|
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemAudioContent(
|
val expected = TimelineItemAudioContent(
|
||||||
|
|
@ -428,7 +428,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = ImageMessageType("body", null, null, MediaSource("url"), null)),
|
content = createMessageContent(type = ImageMessageType("body", null, null, MediaSource("url"), null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemImageContent(
|
val expected = TimelineItemImageContent(
|
||||||
|
|
@ -499,7 +499,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemImageContent(
|
val expected = TimelineItemImageContent(
|
||||||
|
|
@ -524,7 +524,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = FileMessageType("body", MediaSource("url"), null)),
|
content = createMessageContent(type = FileMessageType("body", MediaSource("url"), null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemFileContent(
|
val expected = TimelineItemFileContent(
|
||||||
|
|
@ -559,7 +559,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemFileContent(
|
val expected = TimelineItemFileContent(
|
||||||
|
|
@ -578,7 +578,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = NoticeMessageType("body", null)),
|
content = createMessageContent(type = NoticeMessageType("body", null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemNoticeContent(
|
val expected = TimelineItemNoticeContent(
|
||||||
|
|
@ -601,7 +601,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
assertThat((result as TimelineItemNoticeContent).formattedBody).isEqualTo("formatted")
|
assertThat((result as TimelineItemNoticeContent).formattedBody).isEqualTo("formatted")
|
||||||
|
|
@ -612,7 +612,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
val sut = createTimelineItemContentMessageFactory()
|
val sut = createTimelineItemContentMessageFactory()
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(type = EmoteMessageType("body", null)),
|
content = createMessageContent(type = EmoteMessageType("body", null)),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
val expected = TimelineItemEmoteContent(
|
val expected = TimelineItemEmoteContent(
|
||||||
|
|
@ -635,7 +635,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
formatted = FormattedBody(MessageFormat.HTML, "formatted")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
senderDisplayName = "Bob",
|
senderDisambiguatedDisplayName = "Bob",
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
)
|
)
|
||||||
assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted"))
|
assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted"))
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
|
import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.aProfileTimelineDetailsReady
|
||||||
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||||
|
|
@ -39,7 +40,7 @@ class TimelineItemGrouperTest {
|
||||||
id = "0",
|
id = "0",
|
||||||
senderId = A_USER_ID,
|
senderId = A_USER_ID,
|
||||||
senderAvatar = anAvatarData(),
|
senderAvatar = anAvatarData(),
|
||||||
senderDisplayName = "",
|
senderProfile = aProfileTimelineDetailsReady(displayName = ""),
|
||||||
content = TimelineItemStateEventContent(body = "a state event"),
|
content = TimelineItemStateEventContent(body = "a state event"),
|
||||||
reactionsState = aTimelineItemReactions(count = 0),
|
reactionsState = aTimelineItemReactions(count = 0),
|
||||||
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
|
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class InReplyToDetailTest {
|
class InReplyToDetailTest {
|
||||||
|
|
@ -54,8 +55,7 @@ class InReplyToDetailTest {
|
||||||
val inReplyTo = InReplyTo.Ready(
|
val inReplyTo = InReplyTo.Ready(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
senderId = A_USER_ID,
|
senderId = A_USER_ID,
|
||||||
senderDisplayName = "senderDisplayName",
|
senderProfile = aProfileTimelineDetails(),
|
||||||
senderAvatarUrl = "senderAvatarUrl",
|
|
||||||
content = RoomMembershipContent(
|
content = RoomMembershipContent(
|
||||||
userId = A_USER_ID,
|
userId = A_USER_ID,
|
||||||
change = MembershipChange.INVITED,
|
change = MembershipChange.INVITED,
|
||||||
|
|
@ -73,8 +73,7 @@ class InReplyToDetailTest {
|
||||||
val inReplyTo = InReplyTo.Ready(
|
val inReplyTo = InReplyTo.Ready(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
senderId = A_USER_ID,
|
senderId = A_USER_ID,
|
||||||
senderDisplayName = "senderDisplayName",
|
senderProfile = aProfileTimelineDetails(),
|
||||||
senderAvatarUrl = "senderAvatarUrl",
|
|
||||||
content = MessageContent(
|
content = MessageContent(
|
||||||
body = "**Hello!**",
|
body = "**Hello!**",
|
||||||
inReplyTo = null,
|
inReplyTo = null,
|
||||||
|
|
@ -101,8 +100,7 @@ class InReplyToDetailTest {
|
||||||
val inReplyTo = InReplyTo.Ready(
|
val inReplyTo = InReplyTo.Ready(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
senderId = A_USER_ID,
|
senderId = A_USER_ID,
|
||||||
senderDisplayName = "senderDisplayName",
|
senderProfile = aProfileTimelineDetails(),
|
||||||
senderAvatarUrl = "senderAvatarUrl",
|
|
||||||
content = MessageContent(
|
content = MessageContent(
|
||||||
body = "**Hello!**",
|
body = "**Hello!**",
|
||||||
inReplyTo = null,
|
inReplyTo = null,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageT
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
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.RoomMembershipContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||||
|
|
@ -55,6 +56,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.media.aMediaSource
|
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
|
|
@ -430,15 +432,13 @@ class InReplyToMetadataKtTest {
|
||||||
fun anInReplyToDetails(
|
fun anInReplyToDetails(
|
||||||
eventId: EventId = AN_EVENT_ID,
|
eventId: EventId = AN_EVENT_ID,
|
||||||
senderId: UserId = A_USER_ID,
|
senderId: UserId = A_USER_ID,
|
||||||
senderDisplayName: String? = "senderDisplayName",
|
senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(),
|
||||||
senderAvatarUrl: String? = "senderAvatarUrl",
|
|
||||||
eventContent: EventContent? = aMessageContent(),
|
eventContent: EventContent? = aMessageContent(),
|
||||||
textContent: String? = "textContent",
|
textContent: String? = "textContent",
|
||||||
) = InReplyToDetails(
|
) = InReplyToDetails(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = senderId,
|
senderId = senderId,
|
||||||
senderDisplayName = senderDisplayName,
|
senderProfile = senderProfile,
|
||||||
senderAvatarUrl = senderAvatarUrl,
|
|
||||||
eventContent = eventContent,
|
eventContent = eventContent,
|
||||||
textContent = textContent,
|
textContent = textContent,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
27
features/migration/api/build.gradle.kts
Normal file
27
features/migration/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.migration.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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.api
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
interface MigrationEntryPoint {
|
||||||
|
@Composable
|
||||||
|
fun present(): MigrationState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Render(
|
||||||
|
state: MigrationState,
|
||||||
|
modifier: Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
|
data class MigrationState(
|
||||||
|
val migrationAction: AsyncData<Unit>,
|
||||||
|
)
|
||||||
44
features/migration/impl/build.gradle.kts
Normal file
44
features/migration/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.migration.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.features.migration.api)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(projects.features.rageshake.api)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(projects.features.rageshake.test)
|
||||||
|
}
|
||||||
|
|
@ -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.migration.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.api.MigrationEntryPoint
|
||||||
|
import io.element.android.features.api.MigrationState
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultMigrationEntryPoint @Inject constructor(
|
||||||
|
private val migrationPresenter: MigrationPresenter,
|
||||||
|
) : MigrationEntryPoint {
|
||||||
|
@Composable
|
||||||
|
override fun present(): MigrationState = migrationPresenter.present()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Render(
|
||||||
|
state: MigrationState,
|
||||||
|
modifier: Modifier,
|
||||||
|
) = MigrationView(
|
||||||
|
migrationState = state,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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.migration.impl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_migration")
|
||||||
|
private val applicationMigrationVersion = intPreferencesKey("applicationMigrationVersion")
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultMigrationStore @Inject constructor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
) : MigrationStore {
|
||||||
|
private val store = context.dataStore
|
||||||
|
|
||||||
|
override suspend fun setApplicationMigrationVersion(version: Int) {
|
||||||
|
store.edit { prefs ->
|
||||||
|
prefs[applicationMigrationVersion] = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applicationMigrationVersion(): Flow<Int> {
|
||||||
|
return store.data.map { prefs ->
|
||||||
|
prefs[applicationMigrationVersion] ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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.migration.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.api.MigrationState
|
||||||
|
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class MigrationPresenter @Inject constructor(
|
||||||
|
private val migrationStore: MigrationStore,
|
||||||
|
private val logFilesRemover: LogFilesRemover,
|
||||||
|
) : Presenter<MigrationState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): MigrationState {
|
||||||
|
val migrationStoreVersion = migrationStore.applicationMigrationVersion().collectAsState(initial = null)
|
||||||
|
var migrationAction: AsyncData<Unit> by remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Uncomment this block to run the migration everytime
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
migrationStore.setApplicationMigrationVersion(0)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
LaunchedEffect(migrationStoreVersion.value) {
|
||||||
|
val migrationValue = migrationStoreVersion.value ?: return@LaunchedEffect
|
||||||
|
if (migrationValue == MIGRATION_VERSION) {
|
||||||
|
migrationAction = AsyncData.Success(Unit)
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
migrationAction = AsyncData.Loading(Unit)
|
||||||
|
if (migrationValue < 1) {
|
||||||
|
logFilesRemover.perform()
|
||||||
|
}
|
||||||
|
// Add new step here
|
||||||
|
|
||||||
|
migrationStore.setApplicationMigrationVersion(MIGRATION_VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MigrationState(
|
||||||
|
migrationAction = migrationAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Increment this value when you need to run the migration again, and
|
||||||
|
// add step in the LaunchedEffect above
|
||||||
|
const val MIGRATION_VERSION = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.migration.impl
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.api.MigrationState
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
|
internal class MigrationStateProvider : PreviewParameterProvider<MigrationState> {
|
||||||
|
override val values: Sequence<MigrationState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aMigrationState(),
|
||||||
|
aMigrationState(migrationAction = AsyncData.Loading(Unit)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun aMigrationState(
|
||||||
|
migrationAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||||
|
) = MigrationState(
|
||||||
|
migrationAction = migrationAction,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.migration.impl
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface MigrationStore {
|
||||||
|
/**
|
||||||
|
* Return of flow of the current value for application migration version.
|
||||||
|
* If the value is not set, it will emit 0.
|
||||||
|
* If the emitted value is lower than the current application migration version, it means
|
||||||
|
* that a migration should occur, and at the end [setApplicationMigrationVersion] should be called.
|
||||||
|
*/
|
||||||
|
fun applicationMigrationVersion(): Flow<Int>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the application migration version, typically after a migration has been done.
|
||||||
|
*/
|
||||||
|
suspend fun setApplicationMigrationVersion(version: Int)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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.migration.impl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.api.MigrationState
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MigrationView(
|
||||||
|
migrationState: MigrationState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
if (migrationState.migrationAction.isLoading()) {
|
||||||
|
Text(text = stringResource(id = CommonStrings.common_please_wait))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun MigrationViewPreview(
|
||||||
|
@PreviewParameter(MigrationStateProvider::class) state: MigrationState,
|
||||||
|
) = ElementPreview {
|
||||||
|
MigrationView(
|
||||||
|
migrationState = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.migration.impl
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
class InMemoryMigrationStore(
|
||||||
|
initialApplicationMigrationVersion: Int = 0
|
||||||
|
) : MigrationStore {
|
||||||
|
private val applicationMigrationVersion = MutableStateFlow(initialApplicationMigrationVersion)
|
||||||
|
|
||||||
|
override suspend fun setApplicationMigrationVersion(version: Int) {
|
||||||
|
applicationMigrationVersion.value = version
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applicationMigrationVersion(): Flow<Int> {
|
||||||
|
return applicationMigrationVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* 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.migration.impl
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionMode
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||||
|
import io.element.android.features.rageshake.test.logs.FakeLogFilesRemover
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MigrationPresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - no migration should occurs if ApplicationMigrationVersion is the last one`() = runTest {
|
||||||
|
val store = InMemoryMigrationStore(MigrationPresenter.MIGRATION_VERSION)
|
||||||
|
val presenter = createPresenter(
|
||||||
|
migrationStore = store,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - testing all migrations`() = runTest {
|
||||||
|
val store = InMemoryMigrationStore(0)
|
||||||
|
val logFilesRemoverLambda = lambdaRecorder { -> }
|
||||||
|
val presenter = createPresenter(
|
||||||
|
migrationStore = store,
|
||||||
|
logFilesRemover = FakeLogFilesRemover(logFilesRemoverLambda),
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.migrationAction).isEqualTo(AsyncData.Loading(Unit))
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
|
||||||
|
}
|
||||||
|
logFilesRemoverLambda.assertions().isCalledExactly(1)
|
||||||
|
assertThat(store.applicationMigrationVersion().first()).isEqualTo(MigrationPresenter.MIGRATION_VERSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPresenter(
|
||||||
|
migrationStore: MigrationStore = InMemoryMigrationStore(0),
|
||||||
|
logFilesRemover: LogFilesRemover = FakeLogFilesRemover(lambdaRecorder(ensureNeverCalled = true) { -> }),
|
||||||
|
) = MigrationPresenter(
|
||||||
|
migrationStore = migrationStore,
|
||||||
|
logFilesRemover = logFilesRemover,
|
||||||
|
)
|
||||||
|
|
@ -202,7 +202,7 @@ private fun OnBoardingButtons(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun OnBoardingScreenPreview(
|
internal fun OnBoardingViewPreview(
|
||||||
@PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState
|
@PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
OnBoardingView(
|
OnBoardingView(
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ internal fun PollAnswerView(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
|
internal fun PollAnswerViewDisclosedNotSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(showVotes = true, isSelected = false),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = false),
|
||||||
)
|
)
|
||||||
|
|
@ -140,7 +140,7 @@ internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
|
internal fun PollAnswerViewDisclosedSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(showVotes = true, isSelected = true),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = true),
|
||||||
)
|
)
|
||||||
|
|
@ -148,7 +148,7 @@ internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
|
internal fun PollAnswerViewUndisclosedNotSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(showVotes = false, isSelected = false),
|
answerItem = aPollAnswerItem(showVotes = false, isSelected = false),
|
||||||
)
|
)
|
||||||
|
|
@ -156,7 +156,7 @@ internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
|
internal fun PollAnswerViewUndisclosedSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(showVotes = false, isSelected = true),
|
answerItem = aPollAnswerItem(showVotes = false, isSelected = true),
|
||||||
)
|
)
|
||||||
|
|
@ -164,7 +164,7 @@ internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
|
internal fun PollAnswerViewEndedWinnerNotSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(showVotes = true, isSelected = false, isEnabled = false, isWinner = true),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = false, isEnabled = false, isWinner = true),
|
||||||
)
|
)
|
||||||
|
|
@ -172,7 +172,7 @@ internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
|
internal fun PollAnswerViewEndedWinnerSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = true),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = true),
|
||||||
)
|
)
|
||||||
|
|
@ -180,7 +180,7 @@ internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerEndedSelectedPreview() = ElementPreview {
|
internal fun PollAnswerViewEndedSelectedPreview() = ElementPreview {
|
||||||
PollAnswerView(
|
PollAnswerView(
|
||||||
answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = false),
|
answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = false),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ private fun CreatorView(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollContentUndisclosedPreview() = ElementPreview {
|
internal fun PollContentViewUndisclosedPreview() = ElementPreview {
|
||||||
PollContentView(
|
PollContentView(
|
||||||
eventId = EventId("\$anEventId"),
|
eventId = EventId("\$anEventId"),
|
||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
|
|
@ -258,7 +258,7 @@ internal fun PollContentUndisclosedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollContentDisclosedPreview() = ElementPreview {
|
internal fun PollContentViewDisclosedPreview() = ElementPreview {
|
||||||
PollContentView(
|
PollContentView(
|
||||||
eventId = EventId("\$anEventId"),
|
eventId = EventId("\$anEventId"),
|
||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
|
|
@ -275,7 +275,7 @@ internal fun PollContentDisclosedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollContentEndedPreview() = ElementPreview {
|
internal fun PollContentViewEndedPreview() = ElementPreview {
|
||||||
PollContentView(
|
PollContentView(
|
||||||
eventId = EventId("\$anEventId"),
|
eventId = EventId("\$anEventId"),
|
||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
|
|
@ -292,7 +292,7 @@ internal fun PollContentEndedPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollContentCreatorEditablePreview() = ElementPreview {
|
internal fun PollContentViewCreatorEditablePreview() = ElementPreview {
|
||||||
PollContentView(
|
PollContentView(
|
||||||
eventId = EventId("\$anEventId"),
|
eventId = EventId("\$anEventId"),
|
||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
|
|
@ -309,7 +309,7 @@ internal fun PollContentCreatorEditablePreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollContentCreatorPreview() = ElementPreview {
|
internal fun PollContentViewCreatorPreview() = ElementPreview {
|
||||||
PollContentView(
|
PollContentView(
|
||||||
eventId = EventId("\$anEventId"),
|
eventId = EventId("\$anEventId"),
|
||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
|
|
@ -326,7 +326,7 @@ internal fun PollContentCreatorPreview() = ElementPreview {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollContentCreatorEndedPreview() = ElementPreview {
|
internal fun PollContentViewCreatorEndedPreview() = ElementPreview {
|
||||||
PollContentView(
|
PollContentView(
|
||||||
eventId = EventId("\$anEventId"),
|
eventId = EventId("\$anEventId"),
|
||||||
question = "What type of food should we have at the party?",
|
question = "What type of food should we have at the party?",
|
||||||
|
|
|
||||||
|
|
@ -25,22 +25,36 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
|
||||||
override val values: Sequence<DeveloperSettingsState>
|
override val values: Sequence<DeveloperSettingsState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aDeveloperSettingsState(),
|
aDeveloperSettingsState(),
|
||||||
aDeveloperSettingsState().copy(clearCacheAction = AsyncData.Loading()),
|
aDeveloperSettingsState(
|
||||||
aDeveloperSettingsState().copy(
|
clearCacheAction = AsyncData.Loading()
|
||||||
customElementCallBaseUrlState = CustomElementCallBaseUrlState(
|
),
|
||||||
|
aDeveloperSettingsState(
|
||||||
|
customElementCallBaseUrlState = aCustomElementCallBaseUrlState(
|
||||||
baseUrl = "https://call.element.ahoy",
|
baseUrl = "https://call.element.ahoy",
|
||||||
defaultUrl = "https://call.element.io",
|
|
||||||
validator = { true }
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aDeveloperSettingsState() = DeveloperSettingsState(
|
fun aDeveloperSettingsState(
|
||||||
|
clearCacheAction: AsyncData<Unit> = AsyncData.Uninitialized,
|
||||||
|
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
|
||||||
|
eventSink: (DeveloperSettingsEvents) -> Unit = {},
|
||||||
|
) = DeveloperSettingsState(
|
||||||
features = aFeatureUiModelList(),
|
features = aFeatureUiModelList(),
|
||||||
rageshakeState = aRageshakePreferencesState(),
|
rageshakeState = aRageshakePreferencesState(),
|
||||||
cacheSize = AsyncData.Success("1.2 MB"),
|
cacheSize = AsyncData.Success("1.2 MB"),
|
||||||
clearCacheAction = AsyncData.Uninitialized,
|
clearCacheAction = clearCacheAction,
|
||||||
customElementCallBaseUrlState = CustomElementCallBaseUrlState(baseUrl = null, defaultUrl = "https://call.element.io", validator = { true }),
|
customElementCallBaseUrlState = customElementCallBaseUrlState,
|
||||||
eventSink = {}
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aCustomElementCallBaseUrlState(
|
||||||
|
baseUrl: String? = null,
|
||||||
|
defaultUrl: String = "https://call.element.io",
|
||||||
|
validator: (String?) -> Boolean = { true },
|
||||||
|
) = CustomElementCallBaseUrlState(
|
||||||
|
baseUrl = baseUrl,
|
||||||
|
defaultUrl = defaultUrl,
|
||||||
|
validator = validator,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,21 @@ private fun anEditDefaultNotificationSettingsState(
|
||||||
) = EditDefaultNotificationSettingState(
|
) = EditDefaultNotificationSettingState(
|
||||||
isOneToOne = isOneToOne,
|
isOneToOne = isOneToOne,
|
||||||
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||||
roomsWithUserDefinedMode = persistentListOf(aRoomSummary()),
|
roomsWithUserDefinedMode = persistentListOf(
|
||||||
|
aRoomSummary("Room"),
|
||||||
|
aRoomSummary(null),
|
||||||
|
),
|
||||||
changeNotificationSettingAction = changeNotificationSettingAction,
|
changeNotificationSettingAction = changeNotificationSettingAction,
|
||||||
displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer,
|
displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun aRoomSummary() = RoomSummary.Filled(
|
private fun aRoomSummary(
|
||||||
|
name: String?,
|
||||||
|
) = RoomSummary.Filled(
|
||||||
aRoomSummaryDetails(
|
aRoomSummaryDetails(
|
||||||
roomId = RoomId("!roomId:domain"),
|
roomId = RoomId("!roomId:domain"),
|
||||||
name = "Room",
|
name = name,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
isDirect = false,
|
isDirect = false,
|
||||||
lastMessage = null,
|
lastMessage = null,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
|
|
@ -100,7 +101,11 @@ fun EditDefaultNotificationSettingView(
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(text = summary.details.name)
|
val roomName = summary.details.name
|
||||||
|
Text(
|
||||||
|
text = roomName ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||||
|
fontStyle = FontStyle.Italic.takeIf { roomName == null }
|
||||||
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(text = subtitle)
|
Text(text = subtitle)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* 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.preferences.impl.developer
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.preferences.impl.R
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DeveloperSettingsViewTest {
|
||||||
|
@get:Rule
|
||||||
|
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setDeveloperSettingsView(
|
||||||
|
state = aDeveloperSettingsState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onBackPressed = it
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on element call url open the dialogs and submit emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
|
||||||
|
rule.setDeveloperSettingsView(
|
||||||
|
state = aDeveloperSettingsState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_advanced_settings_element_call_base_url)
|
||||||
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
|
eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.io"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `clicking on open showkase invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setDeveloperSettingsView(
|
||||||
|
state = aDeveloperSettingsState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onOpenShowkase = it
|
||||||
|
)
|
||||||
|
rule.onNodeWithText("Open Showkase browser").performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on configure tracing invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setDeveloperSettingsView(
|
||||||
|
state = aDeveloperSettingsState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
onOpenConfigureTracing = it
|
||||||
|
)
|
||||||
|
rule.onNodeWithText("Configure tracing").performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `clicking on clear cache emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
|
||||||
|
rule.setDeveloperSettingsView(
|
||||||
|
state = aDeveloperSettingsState(
|
||||||
|
eventSink = eventsRecorder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText("Clear cache").performClick()
|
||||||
|
eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView(
|
||||||
|
state: DeveloperSettingsState,
|
||||||
|
onOpenShowkase: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onOpenConfigureTracing: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onBackPressed: () -> Unit = EnsureNeverCalled()
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
DeveloperSettingsView(
|
||||||
|
state = state,
|
||||||
|
onOpenShowkase = onOpenShowkase,
|
||||||
|
onOpenConfigureTracing = onOpenConfigureTracing,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* 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.rageshake.api.logs
|
||||||
|
|
||||||
|
interface LogFilesRemover {
|
||||||
|
suspend fun perform()
|
||||||
|
}
|
||||||
|
|
@ -26,10 +26,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||||
|
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
||||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||||
import io.element.android.features.rageshake.impl.logs.VectorFileLogger
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -40,6 +40,7 @@ class BugReportPresenter @Inject constructor(
|
||||||
private val bugReporter: BugReporter,
|
private val bugReporter: BugReporter,
|
||||||
private val crashDataStore: CrashDataStore,
|
private val crashDataStore: CrashDataStore,
|
||||||
private val screenshotHolder: ScreenshotHolder,
|
private val screenshotHolder: ScreenshotHolder,
|
||||||
|
private val logFilesRemover: LogFilesRemover,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<BugReportState> {
|
) : Presenter<BugReportState> {
|
||||||
private class BugReporterUploadListener(
|
private class BugReporterUploadListener(
|
||||||
|
|
@ -150,6 +151,6 @@ class BugReportPresenter @Inject constructor(
|
||||||
private fun CoroutineScope.resetAll() = launch {
|
private fun CoroutineScope.resetAll() = launch {
|
||||||
screenshotHolder.reset()
|
screenshotHolder.reset()
|
||||||
crashDataStore.reset()
|
crashDataStore.reset()
|
||||||
VectorFileLogger.getFromTimber()?.reset()
|
logFilesRemover.perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.rageshake.impl.logs
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||||
|
import io.element.android.features.rageshake.impl.reporter.DefaultBugReporter
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultLogFilesRemover @Inject constructor(
|
||||||
|
private val bugReporter: DefaultBugReporter,
|
||||||
|
) : LogFilesRemover {
|
||||||
|
override suspend fun perform() {
|
||||||
|
bugReporter.deleteAllFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 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.rageshake.impl.logs
|
|
||||||
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.io.StringWriter
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TimeZone
|
|
||||||
import java.util.logging.Formatter
|
|
||||||
import java.util.logging.LogRecord
|
|
||||||
|
|
||||||
internal class LogFormatter : Formatter() {
|
|
||||||
override fun format(r: LogRecord): String {
|
|
||||||
if (!isTimeZoneSet) {
|
|
||||||
DATE_FORMAT.timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
isTimeZoneSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val thrown = r.thrown
|
|
||||||
if (thrown != null) {
|
|
||||||
val sw = StringWriter()
|
|
||||||
val pw = PrintWriter(sw)
|
|
||||||
sw.write(r.message)
|
|
||||||
sw.write(LINE_SEPARATOR)
|
|
||||||
thrown.printStackTrace(pw)
|
|
||||||
pw.flush()
|
|
||||||
return sw.toString()
|
|
||||||
} else {
|
|
||||||
val b = StringBuilder()
|
|
||||||
val date = DATE_FORMAT.format(Date(r.millis))
|
|
||||||
b.append(date)
|
|
||||||
b.append("Z ")
|
|
||||||
b.append(r.message)
|
|
||||||
b.append(LINE_SEPARATOR)
|
|
||||||
return b.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val LINE_SEPARATOR = System.getProperty("line.separator") ?: "\n"
|
|
||||||
|
|
||||||
// private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
|
|
||||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss*SSSZZZZ", Locale.US)
|
|
||||||
|
|
||||||
private var isTimeZoneSet = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 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.rageshake.impl.logs
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import io.element.android.libraries.androidutils.file.safeDelete
|
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.io.StringWriter
|
|
||||||
import java.util.logging.FileHandler
|
|
||||||
import java.util.logging.Level
|
|
||||||
import java.util.logging.Logger
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will be planted in Timber.
|
|
||||||
*/
|
|
||||||
class VectorFileLogger(
|
|
||||||
private val context: Context,
|
|
||||||
// private val vectorPreferences: VectorPreferences
|
|
||||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
|
||||||
) : Timber.Tree() {
|
|
||||||
companion object {
|
|
||||||
fun getFromTimber(): VectorFileLogger? {
|
|
||||||
return Timber.forest().filterIsInstance<VectorFileLogger>().firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val SIZE_20MB = 20 * 1024 * 1024
|
|
||||||
// private const val SIZE_50MB = 50 * 1024 * 1024
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
private val maxLogSizeByte = if (vectorPreferences.labAllowedExtendedLogging()) SIZE_50MB else SIZE_20MB
|
|
||||||
private val logRotationCount = if (vectorPreferences.labAllowedExtendedLogging()) 15 else 7
|
|
||||||
*/
|
|
||||||
private val maxLogSizeByte = SIZE_20MB
|
|
||||||
private val logRotationCount = 7
|
|
||||||
|
|
||||||
private val logger = Logger.getLogger(context.packageName).apply {
|
|
||||||
tryOrNull {
|
|
||||||
useParentHandlers = false
|
|
||||||
level = Level.ALL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val fileHandler: FileHandler?
|
|
||||||
private val cacheDirectory get() = File(context.cacheDir, "logs").apply {
|
|
||||||
if (!exists()) mkdirs()
|
|
||||||
}
|
|
||||||
private var fileNamePrefix = "logs"
|
|
||||||
|
|
||||||
private val prioPrefixes = mapOf(
|
|
||||||
Log.VERBOSE to "V/ ",
|
|
||||||
Log.DEBUG to "D/ ",
|
|
||||||
Log.INFO to "I/ ",
|
|
||||||
Log.WARN to "W/ ",
|
|
||||||
Log.ERROR to "E/ ",
|
|
||||||
Log.ASSERT to "WTF/ "
|
|
||||||
)
|
|
||||||
|
|
||||||
init {
|
|
||||||
for (i in 0..15) {
|
|
||||||
val file = File(cacheDirectory, "elementLogs.$i.txt")
|
|
||||||
file.safeDelete()
|
|
||||||
}
|
|
||||||
|
|
||||||
fileHandler = tryOrNull(
|
|
||||||
onError = { Timber.e(it, "Failed to initialize FileLogger") }
|
|
||||||
) {
|
|
||||||
FileHandler(
|
|
||||||
cacheDirectory.absolutePath + "/" + fileNamePrefix + ".%g.txt",
|
|
||||||
maxLogSizeByte,
|
|
||||||
logRotationCount
|
|
||||||
)
|
|
||||||
.also { it.formatter = LogFormatter() }
|
|
||||||
.also { logger.addHandler(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
// Delete all files
|
|
||||||
getLogFiles().map {
|
|
||||||
it.safeDelete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
|
||||||
fileHandler ?: return
|
|
||||||
GlobalScope.launch(dispatcher) {
|
|
||||||
if (skipLog(priority)) return@launch
|
|
||||||
if (t != null) {
|
|
||||||
logToFile(t)
|
|
||||||
}
|
|
||||||
logToFile(prioPrefixes[priority] ?: "$priority ", tag ?: "Tag", message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun skipLog(priority: Int): Boolean {
|
|
||||||
// return if (vectorPreferences.labAllowedExtendedLogging()) {
|
|
||||||
// false
|
|
||||||
// } else {
|
|
||||||
// // Exclude verbose logs
|
|
||||||
// priority < Log.DEBUG
|
|
||||||
// }
|
|
||||||
// Exclude verbose logs
|
|
||||||
return priority < Log.DEBUG
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds our own log files to the provided list of files.
|
|
||||||
*
|
|
||||||
* @return The list of files with logs.
|
|
||||||
*/
|
|
||||||
private fun getLogFiles(): List<File> {
|
|
||||||
return tryOrNull(
|
|
||||||
onError = { Timber.e(it, "## getLogFiles() failed") }
|
|
||||||
) {
|
|
||||||
fileHandler
|
|
||||||
?.flush()
|
|
||||||
?.let { 0 until logRotationCount }
|
|
||||||
?.mapNotNull { index ->
|
|
||||||
File(cacheDirectory, "$fileNamePrefix.$index.txt")
|
|
||||||
.takeIf { it.exists() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log an Throwable.
|
|
||||||
*
|
|
||||||
* @param throwable the throwable to log
|
|
||||||
*/
|
|
||||||
private fun logToFile(throwable: Throwable?) {
|
|
||||||
throwable ?: return
|
|
||||||
|
|
||||||
val errors = StringWriter()
|
|
||||||
throwable.printStackTrace(PrintWriter(errors))
|
|
||||||
|
|
||||||
logger.info(errors.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logToFile(level: String, tag: String, content: String) {
|
|
||||||
val b = StringBuilder()
|
|
||||||
b.append(Thread.currentThread().id)
|
|
||||||
b.append(" ")
|
|
||||||
b.append(level)
|
|
||||||
b.append("/")
|
|
||||||
b.append(tag)
|
|
||||||
b.append(": ")
|
|
||||||
b.append(content)
|
|
||||||
logger.info(b.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -346,6 +346,12 @@ class DefaultBugReporter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun deleteAllFiles() {
|
||||||
|
withContext(coroutineDispatchers.io) {
|
||||||
|
getLogFiles().forEach { it.safeDelete() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun setCurrentTracingFilter(tracingFilter: String) {
|
override fun setCurrentTracingFilter(tracingFilter: String) {
|
||||||
currentTracingFilter = tracingFilter
|
currentTracingFilter = tracingFilter
|
||||||
}
|
}
|
||||||
|
|
@ -374,7 +380,6 @@ class DefaultBugReporter @Inject constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all the log files except the most recent one.
|
* Delete all the log files except the most recent one.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
private fun List<File>.deleteAllExceptMostRecent() {
|
private fun List<File>.deleteAllExceptMostRecent() {
|
||||||
if (size > 1) {
|
if (size > 1) {
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,18 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||||
|
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||||
import io.element.android.features.rageshake.test.crash.A_CRASH_DATA
|
import io.element.android.features.rageshake.test.crash.A_CRASH_DATA
|
||||||
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
||||||
|
import io.element.android.features.rageshake.test.logs.FakeLogFilesRemover
|
||||||
import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI
|
import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI
|
||||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -117,9 +120,11 @@ class BugReportPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - reset all`() = runTest {
|
fun `present - reset all`() = runTest {
|
||||||
|
val logFilesRemoverLambda = lambdaRecorder { -> }
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
crashDataStore = FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
|
crashDataStore = FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
|
||||||
screenshotHolder = FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
|
screenshotHolder = FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
|
||||||
|
logFilesRemover = FakeLogFilesRemover(logFilesRemoverLambda),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -131,6 +136,7 @@ class BugReportPresenterTest {
|
||||||
initialState.eventSink.invoke(BugReportEvents.ResetAll)
|
initialState.eventSink.invoke(BugReportEvents.ResetAll)
|
||||||
val resetState = awaitItem()
|
val resetState = awaitItem()
|
||||||
assertThat(resetState.hasCrashLogs).isFalse()
|
assertThat(resetState.hasCrashLogs).isFalse()
|
||||||
|
logFilesRemoverLambda.assertions().isCalledExactly(1)
|
||||||
// TODO Make it live assertThat(resetState.screenshotUri).isNull()
|
// TODO Make it live assertThat(resetState.screenshotUri).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -239,10 +245,12 @@ class BugReportPresenterTest {
|
||||||
bugReporter: BugReporter = FakeBugReporter(),
|
bugReporter: BugReporter = FakeBugReporter(),
|
||||||
crashDataStore: CrashDataStore = FakeCrashDataStore(),
|
crashDataStore: CrashDataStore = FakeCrashDataStore(),
|
||||||
screenshotHolder: ScreenshotHolder = FakeScreenshotHolder(),
|
screenshotHolder: ScreenshotHolder = FakeScreenshotHolder(),
|
||||||
|
logFilesRemover: LogFilesRemover = FakeLogFilesRemover(lambdaRecorder(ensureNeverCalled = true) { -> }),
|
||||||
) = BugReportPresenter(
|
) = BugReportPresenter(
|
||||||
bugReporter,
|
bugReporter = bugReporter,
|
||||||
crashDataStore,
|
crashDataStore = crashDataStore,
|
||||||
screenshotHolder,
|
screenshotHolder = screenshotHolder,
|
||||||
this,
|
logFilesRemover = logFilesRemover,
|
||||||
|
appCoroutineScope = this,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +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.rageshake.impl.logs
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.robolectric.RobolectricTestRunner
|
|
||||||
import org.robolectric.RuntimeEnvironment
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
|
||||||
class VectorFileLoggerTest {
|
|
||||||
@Test
|
|
||||||
fun `init VectorFileLogger log debug`() = runTest {
|
|
||||||
val sut = createVectorFileLogger()
|
|
||||||
sut.d("A debug log")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `init VectorFileLogger log error`() = runTest {
|
|
||||||
val sut = createVectorFileLogger()
|
|
||||||
sut.e(A_THROWABLE, "A debug log")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `reset VectorFileLogger`() = runTest {
|
|
||||||
val sut = createVectorFileLogger()
|
|
||||||
sut.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `check getFromTimber`() {
|
|
||||||
assertThat(VectorFileLogger.getFromTimber()).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TestScope.createVectorFileLogger() = VectorFileLogger(
|
|
||||||
context = RuntimeEnvironment.getApplication(),
|
|
||||||
dispatcher = testCoroutineDispatchers().io,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -24,4 +24,5 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.features.rageshake.api)
|
implementation(projects.features.rageshake.api)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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.rageshake.test.logs
|
||||||
|
|
||||||
|
import io.element.android.features.rageshake.api.logs.LogFilesRemover
|
||||||
|
import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder
|
||||||
|
|
||||||
|
class FakeLogFilesRemover(
|
||||||
|
private val performLambda: LambdaNoParamRecorder<Unit>,
|
||||||
|
) : LogFilesRemover {
|
||||||
|
override suspend fun perform() {
|
||||||
|
performLambda()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.roomaliasresolver.impl"
|
namespace = "io.element.android.features.roomaliasresolver.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anvil {
|
anvil {
|
||||||
|
|
@ -44,10 +49,13 @@ dependencies {
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@
|
||||||
package io.element.android.features.roomaliasresolver.impl
|
package io.element.android.features.roomaliasresolver.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -27,6 +29,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
|
@ -36,6 +39,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
|
import io.element.android.libraries.designsystem.background.LightGradientBackground
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -61,22 +65,26 @@ fun RoomAliasResolverView(
|
||||||
latestOnAliasResolved(state.resolveState.data)
|
latestOnAliasResolved(state.resolveState.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Box(
|
||||||
HeaderFooterPage(
|
modifier = modifier.fillMaxSize(),
|
||||||
modifier = modifier,
|
) {
|
||||||
paddingValues = PaddingValues(16.dp),
|
LightGradientBackground()
|
||||||
topBar = {
|
HeaderFooterPage(
|
||||||
RoomAliasResolverTopBar(onBackClicked = onBackPressed)
|
containerColor = Color.Transparent,
|
||||||
},
|
paddingValues = PaddingValues(16.dp),
|
||||||
content = {
|
topBar = {
|
||||||
RoomAliasResolverContent(state = state)
|
RoomAliasResolverTopBar(onBackClicked = onBackPressed)
|
||||||
},
|
},
|
||||||
footer = {
|
content = {
|
||||||
RoomAliasResolverFooter(
|
RoomAliasResolverContent(state = state)
|
||||||
state = state,
|
},
|
||||||
)
|
footer = {
|
||||||
}
|
RoomAliasResolverFooter(
|
||||||
)
|
state = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -92,7 +100,7 @@ private fun RoomAliasResolverFooter(
|
||||||
state.eventSink(RoomAliasResolverEvents.Retry)
|
state.eventSink(RoomAliasResolverEvents.Retry)
|
||||||
},
|
},
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Large,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AsyncData.Loading -> {
|
is AsyncData.Loading -> {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_room_alias_resolver_resolve_alias_failure">"Не ўдалося разабрацца з псеўданімам пакоя."</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_room_alias_resolver_resolve_alias_failure">"Nem sikerült a szoba álnevének feloldása."</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_room_alias_resolver_resolve_alias_failure">"Не удалось определить псевдоним комнаты."</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_room_alias_resolver_resolve_alias_failure">"Nepodarilo sa nájsť alias miestnosti."</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* 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.roomaliasresolver.impl
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.clickOn
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
|
import io.element.android.tests.testutils.pressBack
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class RoomAliasResolverViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invokes the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setRoomAliasResolverView(
|
||||||
|
aRoomAliasResolverState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackPressed = it
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Retry emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>()
|
||||||
|
rule.setRoomAliasResolverView(
|
||||||
|
aRoomAliasResolverState(
|
||||||
|
resolveState = AsyncData.Failure(Exception("Error")),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_retry)
|
||||||
|
eventsRecorder.assertSingle(RoomAliasResolverEvents.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `success state invokes the expected Callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnceWithParam(A_ROOM_ID) {
|
||||||
|
rule.setRoomAliasResolverView(
|
||||||
|
aRoomAliasResolverState(
|
||||||
|
resolveState = AsyncData.Success(A_ROOM_ID),
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onAliasResolved = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomAliasResolverView(
|
||||||
|
state: RoomAliasResolverState,
|
||||||
|
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onAliasResolved: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
RoomAliasResolverView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
onAliasResolved = onAliasResolved,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -225,7 +225,7 @@ private fun RoomInviteMembersSearchBar(
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomInviteMembersPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) = ElementPreview {
|
internal fun RoomInviteMembersViewPreview(@PreviewParameter(RoomInviteMembersStateProvider::class) state: RoomInviteMembersState) = ElementPreview {
|
||||||
RoomInviteMembersView(
|
RoomInviteMembersView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackPressed = {},
|
onBackPressed = {},
|
||||||
|
|
|
||||||
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