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

View File

@@ -0,0 +1,19 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.accountselect.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}

View File

@@ -0,0 +1,28 @@
/*
* 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.libraries.accountselect.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.SessionId
interface AccountSelectEntryPoint : FeatureEntryPoint {
fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: Callback,
): Node
interface Callback : Plugin {
fun onAccountSelected(sessionId: SessionId)
fun onCancel()
}
}

View File

@@ -0,0 +1,36 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* 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.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.accountselect.impl"
}
setupDependencyInjection()
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.sessionStorage.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
api(projects.libraries.accountselect.api)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.test)
}

View File

@@ -0,0 +1,42 @@
/*
* 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.libraries.accountselect.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.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.architecture.callback
@ContributesNode(AppScope::class)
@AssistedInject
class AccountSelectNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AccountSelectPresenter,
) : Node(buildContext, plugins = plugins) {
private val callback: AccountSelectEntryPoint.Callback = callback()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
AccountSelectView(
state = state,
onDismiss = callback::onCancel,
onSelectAccount = callback::onAccountSelected,
modifier = modifier,
)
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.libraries.accountselect.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@Inject
class AccountSelectPresenter(
private val sessionStore: SessionStore,
) : Presenter<AccountSelectState> {
@Composable
override fun present(): AccountSelectState {
val accounts by produceState<ImmutableList<MatrixUser>>(persistentListOf()) {
// Do not use sessionStore.sessionsFlow() to not make it change when an account is selected.
value = sessionStore.getAllSessions()
.map {
MatrixUser(
userId = UserId(it.userId),
displayName = it.userDisplayName,
avatarUrl = it.userAvatarUrl,
)
}
.toImmutableList()
}
return AccountSelectState(
accounts = accounts,
)
}
}

View File

@@ -0,0 +1,16 @@
/*
* 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.libraries.accountselect.impl
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class AccountSelectState(
val accounts: ImmutableList<MatrixUser>,
)

View File

@@ -0,0 +1,28 @@
/*
* 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.libraries.accountselect.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import kotlinx.collections.immutable.toImmutableList
open class AccountSelectStateProvider : PreviewParameterProvider<AccountSelectState> {
override val values: Sequence<AccountSelectState>
get() = sequenceOf(
anAccountSelectState(),
anAccountSelectState(accounts = aMatrixUserList()),
)
}
private fun anAccountSelectState(
accounts: List<MatrixUser> = listOf(),
) = AccountSelectState(
accounts = accounts.toImmutableList(),
)

View File

@@ -0,0 +1,89 @@
/*
* 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.libraries.accountselect.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
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.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.ui.strings.CommonStrings
@Suppress("MultipleEmitters") // False positive
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountSelectView(
state: AccountSelectState,
onSelectAccount: (SessionId) -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = { onDismiss() })
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
titleStr = stringResource(CommonStrings.common_select_account),
navigationIcon = {
BackButton(onClick = { onDismiss() })
},
)
}
) { paddingValues ->
Column(
Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
) {
LazyColumn {
items(state.accounts, key = { it.userId }) { matrixUser ->
Column {
MatrixUserRow(
modifier = Modifier
.fillMaxWidth()
.clickable {
onSelectAccount(matrixUser.userId)
}
.padding(vertical = 8.dp),
matrixUser = matrixUser,
)
HorizontalDivider()
}
}
}
}
}
}
@PreviewsDayNight
@Composable
internal fun AccountSelectViewPreview(@PreviewParameter(AccountSelectStateProvider::class) state: AccountSelectState) = ElementPreview {
AccountSelectView(
state = state,
onSelectAccount = {},
onDismiss = {},
)
}

View File

@@ -0,0 +1,27 @@
/*
* 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.libraries.accountselect.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.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultAccountSelectEntryPoint : AccountSelectEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: AccountSelectEntryPoint.Callback,
): Node {
return parentNode.createNode<AccountSelectNode>(buildContext, listOf(callback))
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.libraries.accountselect.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class AccountSelectPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createAccountSelectPresenter()
presenter.test {
val initialState = awaitItem()
assertThat(initialState.accounts).isEmpty()
}
}
@Test
fun `present - multiple accounts case`() = runTest {
val presenter = createAccountSelectPresenter(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_SESSION_ID.value),
aSessionData(
sessionId = A_SESSION_ID_2.value,
userDisplayName = "Bob",
userAvatarUrl = "avatarUrl",
),
)
)
)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.accounts).hasSize(2)
val firstAccount = initialState.accounts[0]
assertThat(firstAccount).isEqualTo(
MatrixUser(
userId = A_SESSION_ID,
displayName = null,
avatarUrl = null,
)
)
val secondAccount = initialState.accounts[1]
assertThat(secondAccount).isEqualTo(
MatrixUser(
userId = A_SESSION_ID_2,
displayName = "Bob",
avatarUrl = "avatarUrl",
)
)
}
}
}
internal fun createAccountSelectPresenter(
sessionStore: SessionStore = InMemorySessionStore(),
) = AccountSelectPresenter(
sessionStore = sessionStore,
)

View File

@@ -0,0 +1,47 @@
/*
* 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.libraries.accountselect.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.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.matrix.api.core.SessionId
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 DefaultAccountSelectEntryPointTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun `test node builder`() {
val entryPoint = DefaultAccountSelectEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
AccountSelectNode(
buildContext = buildContext,
plugins = plugins,
presenter = createAccountSelectPresenter(),
)
}
val callback = object : AccountSelectEntryPoint.Callback {
override fun onAccountSelected(sessionId: SessionId) = lambdaError()
override fun onCancel() = lambdaError()
}
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
callback = callback,
)
assertThat(result).isInstanceOf(AccountSelectNode::class.java)
assertThat(result.plugins).contains(callback)
}
}