First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
@@ -0,0 +1,40 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.roomaliasresolver.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
api(projects.features.roomaliasresolver.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultRoomAliasResolverEntryPoint : RoomAliasResolverEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: RoomAliasResolverEntryPoint.Params,
callback: RoomAliasResolverEntryPoint.Callback,
): Node {
return parentNode.createNode<RoomAliasResolverNode>(
buildContext = buildContext,
plugins = listOf(params, callback),
)
}
}
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl
sealed interface RoomAliasResolverEvents {
data object Retry : RoomAliasResolverEvents
data object DismissError : RoomAliasResolverEvents
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class RoomAliasResolverNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: RoomAliasResolverPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
private val callback: RoomAliasResolverEntryPoint.Callback = callback()
private val inputs = inputs<RoomAliasResolverEntryPoint.Params>()
private val presenter = presenterFactory.create(
inputs.roomAlias
)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
RoomAliasResolverView(
state = state,
onSuccess = callback::onAliasResolved,
onBackClick = ::navigateUp,
modifier = modifier
)
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.jvm.optionals.getOrElse
@AssistedInject
class RoomAliasResolverPresenter(
@Assisted private val roomAlias: RoomAlias,
private val matrixClient: MatrixClient,
) : Presenter<RoomAliasResolverState> {
fun interface Factory {
fun create(
roomAlias: RoomAlias,
): RoomAliasResolverPresenter
}
@Composable
override fun present(): RoomAliasResolverState {
val coroutineScope = rememberCoroutineScope()
val resolveState: MutableState<AsyncData<ResolvedRoomAlias>> = remember { mutableStateOf(AsyncData.Uninitialized) }
LaunchedEffect(Unit) {
resolveAlias(resolveState)
}
fun handleEvent(event: RoomAliasResolverEvents) {
when (event) {
RoomAliasResolverEvents.Retry -> coroutineScope.resolveAlias(resolveState)
RoomAliasResolverEvents.DismissError -> resolveState.value = AsyncData.Uninitialized
}
}
return RoomAliasResolverState(
roomAlias = roomAlias,
resolveState = resolveState.value,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.resolveAlias(resolveState: MutableState<AsyncData<ResolvedRoomAlias>>) = launch {
suspend {
matrixClient.resolveRoomAlias(roomAlias)
.getOrThrow()
.getOrElse { throw RoomAliasResolverFailures.UnknownAlias }
}.runCatchingUpdatingState(resolveState)
}
}
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
data class RoomAliasResolverState(
val roomAlias: RoomAlias,
val resolveState: AsyncData<ResolvedRoomAlias>,
val eventSink: (RoomAliasResolverEvents) -> Unit
)
sealed class RoomAliasResolverFailures : Exception() {
data object UnknownAlias : RoomAliasResolverFailures()
}
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
open class RoomAliasResolverStateProvider : PreviewParameterProvider<RoomAliasResolverState> {
override val values: Sequence<RoomAliasResolverState>
get() = sequenceOf(
aRoomAliasResolverState(),
aRoomAliasResolverState(
resolveState = AsyncData.Failure(ClientException.Generic("Something went wrong", null)),
),
aRoomAliasResolverState(
resolveState = AsyncData.Failure(RoomAliasResolverFailures.UnknownAlias),
),
)
}
fun aRoomAliasResolverState(
roomAlias: RoomAlias = A_ROOM_ALIAS,
resolveState: AsyncData<ResolvedRoomAlias> = AsyncData.Uninitialized,
eventSink: (RoomAliasResolverEvents) -> Unit = {}
) = RoomAliasResolverState(
roomAlias = roomAlias,
resolveState = resolveState,
eventSink = eventSink,
)
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")
@@ -0,0 +1,156 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
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.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
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.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RoomAliasResolverView(
state: RoomAliasResolverState,
onBackClick: () -> Unit,
onSuccess: (ResolvedRoomAlias) -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxSize(),
) {
HeaderFooterPage(
containerColor = Color.Transparent,
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 32.dp
),
topBar = {
RoomAliasResolverTopBar(onBackClick = onBackClick)
},
content = {
RoomAliasResolverContent(roomAlias = state.roomAlias, isLoading = state.resolveState.isLoading())
},
)
ResolvedRoomAliasView(
resolvedRoomAlias = state.resolveState,
onSuccess = onSuccess,
onRetry = { state.eventSink(RoomAliasResolverEvents.Retry) },
onDismissError = {
state.eventSink(RoomAliasResolverEvents.DismissError)
onBackClick()
}
)
}
}
@Composable
private fun ResolvedRoomAliasView(
resolvedRoomAlias: AsyncData<ResolvedRoomAlias>,
onSuccess: (ResolvedRoomAlias) -> Unit,
onRetry: () -> Unit,
onDismissError: () -> Unit,
) {
when (resolvedRoomAlias) {
is AsyncData.Success -> {
val latestOnSuccess by rememberUpdatedState(onSuccess)
LaunchedEffect(Unit) {
latestOnSuccess(resolvedRoomAlias.data)
}
}
is AsyncData.Failure -> {
if (resolvedRoomAlias.error is RoomAliasResolverFailures.UnknownAlias) {
ErrorDialog(
title = stringResource(id = R.string.screen_join_room_loading_alert_title),
content = stringResource(id = R.string.screen_room_alias_resolver_resolve_alias_failure),
onSubmit = onDismissError
)
} else {
RetryDialog(
title = stringResource(id = R.string.screen_join_room_loading_alert_title),
content = stringResource(id = CommonStrings.error_network_or_server_issue),
onRetry = onRetry,
onDismiss = onDismissError
)
}
}
else -> Unit
}
}
@Composable
private fun RoomAliasResolverContent(
roomAlias: RoomAlias,
isLoading: Boolean,
modifier: Modifier = Modifier,
) {
RoomPreviewOrganism(
modifier = modifier,
avatar = {
PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp)
},
title = {
RoomPreviewSubtitleAtom(roomAlias.value)
},
subtitle = {
if (isLoading) {
Spacer(Modifier.height(8.dp))
CircularProgressIndicator()
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomAliasResolverTopBar(
onBackClick: () -> Unit,
) {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackClick)
},
title = {},
)
}
@PreviewsDayNight
@Composable
internal fun RoomAliasResolverViewPreview(@PreviewParameter(RoomAliasResolverStateProvider::class) state: RoomAliasResolverState) = ElementPreview {
RoomAliasResolverView(
state = state,
onSuccess = { },
onBackClick = { }
)
}
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import io.element.android.features.roomaliasresolver.impl.RoomAliasResolverPresenter
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias
@BindingContainer
@ContributesTo(SessionScope::class)
object RoomAliasResolverModule {
@Provides
fun providesJoinRoomPresenterFactory(
client: MatrixClient,
): RoomAliasResolverPresenter.Factory {
return object : RoomAliasResolverPresenter.Factory {
override fun create(roomAlias: RoomAlias): RoomAliasResolverPresenter {
return RoomAliasResolverPresenter(
roomAlias = roomAlias,
matrixClient = client,
)
}
}
}
}
@@ -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,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Náhled této místnosti jsme nemohli zobrazit"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Nepodařilo se přeložit alias místnosti."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Doedd dim modd dangos rhagolwg yr ystafell hon"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Wedi methu â datrys arallenw ystafell."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Vi kunne ikke forhåndsvise rummet"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Kunne ikke løse rummets alias."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Wir konnten diese Chat-Vorschau nicht anzeigen"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Der Chat-Alias konnte nicht ermittelt werden."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Δεν μπορέσαμε να εμφανίσουμε αυτή την προεπισκόπηση αίθουσας"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Αποτυχία επίλυσης του ψευδώνυμου αίθουσας."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"No hemos podido mostrar la vista previa de esta sala"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"No se pudo resolver el alias de la sala."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Meil ei õnnestunud selle jututoa eelvaadet kuvada"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Jututoa aliasele vastava aadressi tuvastamine ei õnnestunud."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Emme voineet näyttää tämän huoneen esikatselua"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Huoneen aliaksen ratkaiseminen epäonnistui."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Impossible dafficher laperçu de ce salon"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Impossible de trouver un salon avec cet alias."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Nem tudtuk megjeleníteni a szoba előnézetét"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Nem sikerült a szoba álnevének feloldása."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Kami tidak dapat menampilkan pratinjau ruangan ini"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Gagal menyelesaikan alias ruangan."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Non è stato possibile visualizzare l\'anteprima di questa stanza"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Impossibile risolvere l\'alias della stanza."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"이 방 미리보기를 표시할 수 없습니다."</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"방 별칭을 확인할 수 없습니다."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Vi kunne ikke vise forhåndsvisning av dette rommet"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Kunne ikke løse romalias."</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">"Kan het kameradres niet vinden."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Nie udało nam się wyświetlić podglądu tego pokoju"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Nie udało się uzyskać aliasu pokoju."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Não foi possível exibir a pré-visualização desta sala"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Falha ao descobrir o alias da sala."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Não foi possível exibir a pré-visualização desta sala"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Não foi possível encontrar esse endereço de sala"</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Nu am putut afișa previzualizarea acestei camere."</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Nu s-a putut rezolva alias-ul camerei."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Мы не смогли отобразить предварительный просмотр этой комнаты"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Не удалось определить псевдоним комнаты."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Ukážku tejto miestnosti sa nepodarilo zobraziť"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Nepodarilo sa nájsť alias miestnosti."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Vi kunde inte visa förhandsgranskningen av rummet"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Misslyckades med att slå upp rumsalias."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Bu oda önizlemesini görüntüleyemedik"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Oda takma adı çözümlenemedi."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Ми не можемо показати попередній перегляд цієї кімнати"</string>
<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">"کمرے کے عرف کو حل کرنے میں ناکام۔"</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"Biz bu xonani oldindan korishni korsata olmadik "</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Xona taxalluslari yechilmadi."</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"我們無法顯示此聊天室的預覽"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"無法解析聊天室別名。"</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"无法显示此房间预览"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"无法解析聊天室别名。"</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_loading_alert_title">"We couldnt display this room preview"</string>
<string name="screen_room_alias_resolver_resolve_alias_failure">"Failed to resolve room alias."</string>
</resources>
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import org.junit.Rule
import org.junit.Test
class DefaultRoomAliasResolverEntryPointTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun `test node builder`() {
val entryPoint = DefaultRoomAliasResolverEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
RoomAliasResolverNode(
buildContext = buildContext,
plugins = plugins,
presenterFactory = { alias ->
assertThat(alias).isEqualTo(A_ROOM_ALIAS)
createPresenter(
alias,
)
}
)
}
val callback = object : RoomAliasResolverEntryPoint.Callback {
override fun onAliasResolved(data: ResolvedRoomAlias) = lambdaError()
}
val params = RoomAliasResolverEntryPoint.Params(
roomAlias = A_ROOM_ALIAS
)
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
params = params,
callback = callback,
)
assertThat(result).isInstanceOf(RoomAliasResolverNode::class.java)
assertThat(result.plugins).contains(params)
assertThat(result.plugins).contains(callback)
}
}
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomaliasresolver.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.libraries.matrix.api.MatrixClient
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.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import java.util.Optional
class RoomAliasHelperPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - resolve alias to roomId`() = runTest {
val result = Optional.of(aResolvedRoomAlias())
val client = FakeMatrixClient(
resolveRoomAliasResult = { Result.success(result) }
)
val presenter = createPresenter(matrixClient = client)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
assertThat(awaitItem().resolveState.isLoading()).isTrue()
val resultState = awaitItem()
assertThat(resultState.roomAlias).isEqualTo(A_ROOM_ALIAS)
assertThat(resultState.resolveState.dataOrNull()).isEqualTo(result.get())
}
}
@Test
fun `present - resolve alias error and retry`() = runTest {
val client = FakeMatrixClient(
resolveRoomAliasResult = { Result.failure(AN_EXCEPTION) }
)
val presenter = createPresenter(matrixClient = client)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().resolveState.isUninitialized()).isTrue()
assertThat(awaitItem().resolveState.isLoading()).isTrue()
val resultState = awaitItem()
assertThat(resultState.resolveState.errorOrNull()).isEqualTo(AN_EXCEPTION)
resultState.eventSink(RoomAliasResolverEvents.Retry)
val retryLoadingState = awaitItem()
assertThat(retryLoadingState.resolveState.isLoading()).isTrue()
val retryState = awaitItem()
assertThat(retryState.resolveState.errorOrNull()).isEqualTo(AN_EXCEPTION)
}
}
}
internal fun createPresenter(
roomAlias: RoomAlias = A_ROOM_ALIAS,
matrixClient: MatrixClient = FakeMatrixClient(),
) = RoomAliasResolverPresenter(
roomAlias = roomAlias,
matrixClient = matrixClient,
)
internal fun aResolvedRoomAlias(
roomId: RoomId = A_ROOM_ID,
servers: List<String> = A_SERVER_LIST,
) = ResolvedRoomAlias(
roomId = roomId,
servers = servers,
)
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
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.room.alias.ResolvedRoomAlias
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 RoomAliasHelperViewTest {
@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,
),
onBackClick = 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 result = aResolvedRoomAlias()
val eventsRecorder = EventsRecorder<RoomAliasResolverEvents>(expectEvents = false)
ensureCalledOnceWithParam(result) {
rule.setRoomAliasResolverView(
aRoomAliasResolverState(
resolveState = AsyncData.Success(result),
eventSink = eventsRecorder,
),
onAliasResolved = it,
)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomAliasResolverView(
state: RoomAliasResolverState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onAliasResolved: (ResolvedRoomAlias) -> Unit = EnsureNeverCalledWithParam(),
) {
setContent {
RoomAliasResolverView(
state = state,
onBackClick = onBackClick,
onSuccess = onAliasResolved,
)
}
}