Merge branch 'develop' into feature/fga/clean_up
This commit is contained in:
commit
7e25371bf2
252 changed files with 5060 additions and 1618 deletions
|
|
@ -33,7 +33,8 @@ plugins {
|
||||||
id("com.google.firebase.appdistribution") version "4.0.0"
|
id("com.google.firebase.appdistribution") version "4.0.0"
|
||||||
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
id("com.google.gms.google-services")
|
// To be able to update the firebase.xml files, uncomment and build the project
|
||||||
|
// id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -205,6 +206,7 @@ dependencies {
|
||||||
allLibrariesImpl()
|
allLibrariesImpl()
|
||||||
allServicesImpl()
|
allServicesImpl()
|
||||||
allFeaturesImpl(rootDir)
|
allFeaturesImpl(rootDir)
|
||||||
|
implementation(projects.libraries.deeplink)
|
||||||
implementation(projects.tests.uitests)
|
implementation(projects.tests.uitests)
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
implementation(projects.appnav)
|
implementation(projects.appnav)
|
||||||
|
|
@ -225,9 +227,6 @@ dependencies {
|
||||||
implementation(platform(libs.network.okhttp.bom))
|
implementation(platform(libs.network.okhttp.bom))
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||||
|
|
||||||
implementation(platform(libs.google.firebase.bom))
|
|
||||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
|
||||||
|
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
kapt(libs.dagger.compiler)
|
kapt(libs.dagger.compiler)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "912726360885",
|
|
||||||
"firebase_url": "https://vector-alpha.firebaseio.com",
|
|
||||||
"project_id": "vector-alpha",
|
|
||||||
"storage_bucket": "vector-alpha.appspot.com"
|
|
||||||
},
|
|
||||||
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:912726360885:android:def0a4e454042e9b00427c",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "io.element.android.x.debug"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-hvgoj23p6plt7hikhtdrakihojghaftv.apps.googleusercontent.com",
|
|
||||||
"client_type": 1,
|
|
||||||
"android_info": {
|
|
||||||
"package_name": "io.element.android.x.debug",
|
|
||||||
"certificate_hash": "41bd63b3b612a15d9ba36a5245c393f2a9b992d1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
|
|
@ -40,6 +40,14 @@
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Handle deep-link for notification, uncomment to be able to test deeplink with ./tools/adb/deeplink.sh -->
|
||||||
|
<!--intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data
|
||||||
|
android:host="open"
|
||||||
|
android:scheme="elementx" />
|
||||||
|
</intent-filter-->
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.bumble.appyx.core.integration.NodeHost
|
import com.bumble.appyx.core.integration.NodeHost
|
||||||
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
||||||
|
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||||
import io.element.android.x.di.AppBindings
|
import io.element.android.x.di.AppBindings
|
||||||
|
|
@ -35,6 +36,8 @@ import timber.log.Timber
|
||||||
|
|
||||||
class MainActivity : NodeComponentActivity() {
|
class MainActivity : NodeComponentActivity() {
|
||||||
|
|
||||||
|
lateinit var mainNode: MainNode
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -44,10 +47,23 @@ class MainActivity : NodeComponentActivity() {
|
||||||
setContent {
|
setContent {
|
||||||
ElementTheme {
|
ElementTheme {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background),
|
||||||
) {
|
) {
|
||||||
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
||||||
MainNode(it, appBindings.mainDaggerComponentOwner())
|
MainNode(
|
||||||
|
it,
|
||||||
|
appBindings.mainDaggerComponentOwner(),
|
||||||
|
plugins = listOf(
|
||||||
|
object : NodeReadyObserver<MainNode> {
|
||||||
|
override fun init(node: MainNode) {
|
||||||
|
mainNode = node
|
||||||
|
mainNode.handleIntent(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,6 +79,8 @@ class MainActivity : NodeComponentActivity() {
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
Timber.w("onNewIntent")
|
Timber.w("onNewIntent")
|
||||||
|
intent ?: return
|
||||||
|
mainNode.handleIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,17 @@
|
||||||
|
|
||||||
package io.element.android.x
|
package io.element.android.x
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.node.ParentNode
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.appnav.LoggedInFlowNode
|
import io.element.android.appnav.LoggedInFlowNode
|
||||||
import io.element.android.appnav.RoomFlowNode
|
import io.element.android.appnav.RoomFlowNode
|
||||||
import io.element.android.appnav.RootFlowNode
|
import io.element.android.appnav.RootFlowNode
|
||||||
|
|
@ -35,11 +38,13 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.x.di.MainDaggerComponentsOwner
|
import io.element.android.x.di.MainDaggerComponentsOwner
|
||||||
import io.element.android.x.di.RoomComponent
|
import io.element.android.x.di.RoomComponent
|
||||||
import io.element.android.x.di.SessionComponent
|
import io.element.android.x.di.SessionComponent
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
class MainNode(
|
class MainNode(
|
||||||
buildContext: BuildContext,
|
buildContext: BuildContext,
|
||||||
private val mainDaggerComponentOwner: MainDaggerComponentsOwner,
|
private val mainDaggerComponentOwner: MainDaggerComponentsOwner,
|
||||||
|
plugins: List<Plugin>,
|
||||||
) :
|
) :
|
||||||
ParentNode<MainNode.RootNavTarget>(
|
ParentNode<MainNode.RootNavTarget>(
|
||||||
navModel = PermanentNavModel(
|
navModel = PermanentNavModel(
|
||||||
|
|
@ -47,6 +52,7 @@ class MainNode(
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
),
|
),
|
||||||
DaggerComponentOwner by mainDaggerComponentOwner {
|
DaggerComponentOwner by mainDaggerComponentOwner {
|
||||||
|
|
||||||
|
|
@ -73,7 +79,13 @@ class MainNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node {
|
||||||
return createNode<RootFlowNode>(buildContext, plugins = listOf(loggedInFlowNodeCallback, roomFlowNodeCallback))
|
return createNode<RootFlowNode>(
|
||||||
|
context = buildContext,
|
||||||
|
plugins = listOf(
|
||||||
|
loggedInFlowNodeCallback,
|
||||||
|
roomFlowNodeCallback,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -81,6 +93,12 @@ class MainNode(
|
||||||
Children(navModel = navModel)
|
Children(navModel = navModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleIntent(intent: Intent) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
waitForChildAttached<RootFlowNode>().handleIntent(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object RootNavTarget : Parcelable
|
object RootNavTarget : Parcelable
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ package io.element.android.x.intent
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.deeplink.DeepLinkCreator
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -28,17 +30,19 @@ import io.element.android.libraries.push.impl.intent.IntentProvider
|
||||||
import io.element.android.x.MainActivity
|
import io.element.android.x.MainActivity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// TODO EAx change to deep-link.
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class IntentProviderImpl @Inject constructor(
|
class IntentProviderImpl @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
|
private val deepLinkCreator: DeepLinkCreator,
|
||||||
) : IntentProvider {
|
) : IntentProvider {
|
||||||
override fun getMainIntent(): Intent {
|
override fun getViewIntent(
|
||||||
return Intent(context, MainActivity::class.java)
|
sessionId: SessionId,
|
||||||
}
|
roomId: RoomId?,
|
||||||
|
threadId: ThreadId?,
|
||||||
override fun getIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): Intent {
|
): Intent {
|
||||||
// TODO Handle deeplink or pass parameters
|
return Intent(context, MainActivity::class.java).apply {
|
||||||
return Intent(context, MainActivity::class.java)
|
action = Intent.ACTION_VIEW
|
||||||
|
data = deepLinkCreator.create(sessionId, roomId, threadId).toUri()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "912726360885",
|
|
||||||
"firebase_url": "https://vector-alpha.firebaseio.com",
|
|
||||||
"project_id": "vector-alpha",
|
|
||||||
"storage_bucket": "vector-alpha.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:912726360885:android:e17435e0beb0303000427c",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "io.element.android.x.nightly"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "912726360885",
|
|
||||||
"firebase_url": "https://vector-alpha.firebaseio.com",
|
|
||||||
"project_id": "vector-alpha",
|
|
||||||
"storage_bucket": "vector-alpha.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:912726360885:android:d097de99a4c23d2700427c",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "io.element.android.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
|
|
@ -43,8 +43,10 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.deeplink)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.push.api)
|
implementation(projects.libraries.push.api)
|
||||||
|
implementation(projects.libraries.pushproviders.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
|
@ -214,6 +215,19 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun attachRoot(): Node {
|
||||||
|
return attachChild {
|
||||||
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun attachRoom(roomId: RoomId): RoomFlowNode {
|
||||||
|
return attachChild {
|
||||||
|
backstack.singleTop(NavTarget.RoomList)
|
||||||
|
backstack.push(NavTarget.Room(roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -45,6 +46,8 @@ import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
|
import io.element.android.libraries.deeplink.DeeplinkParser
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
|
@ -65,6 +68,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private val matrixClientsHolder: MatrixClientsHolder,
|
private val matrixClientsHolder: MatrixClientsHolder,
|
||||||
private val presenter: RootPresenter,
|
private val presenter: RootPresenter,
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
|
private val deeplinkParser: DeeplinkParser,
|
||||||
) :
|
) :
|
||||||
BackstackNode<RootFlowNode.NavTarget>(
|
BackstackNode<RootFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -207,4 +211,30 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun handleIntent(intent: Intent) {
|
||||||
|
deeplinkParser.getFromIntent(intent)
|
||||||
|
?.let { navigateTo(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
||||||
|
Timber.d("Navigating to $deeplinkData")
|
||||||
|
attachSession(deeplinkData.sessionId)
|
||||||
|
.apply {
|
||||||
|
val roomId = deeplinkData.roomId
|
||||||
|
if (roomId == null) {
|
||||||
|
// In case room is not provided, ensure the app navigate back to the room list
|
||||||
|
attachRoot()
|
||||||
|
} else {
|
||||||
|
attachRoom(roomId)
|
||||||
|
// TODO .attachThread(deeplinkData.threadId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||||
|
return attachChild {
|
||||||
|
backstack.newRoot(NavTarget.LoggedInFlow(sessionId))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,10 @@ class LoggedInPresenter @Inject constructor(
|
||||||
override fun present(): LoggedInState {
|
override fun present(): LoggedInState {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Ensure pusher is registered
|
// Ensure pusher is registered
|
||||||
pushService.registerFirebasePusher(matrixClient)
|
// TODO Manually select push provider for now
|
||||||
|
val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect
|
||||||
|
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||||
}
|
}
|
||||||
|
|
||||||
val permissionsState = postNotificationPermissionsPresenter.present()
|
val permissionsState = postNotificationPermissionsPresenter.present()
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ fun LoggedInView(
|
||||||
state = state.permissionsState,
|
state = state.permissionsState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
openSystemSettings = {
|
openSystemSettings = {
|
||||||
activity?.let { openAppSettingsPage(it, "") }
|
activity?.let { openAppSettingsPage(it) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
|
import io.element.android.libraries.push.providers.api.Distributor
|
||||||
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -55,7 +57,11 @@ class LoggedInPresenterTest {
|
||||||
override fun notificationStyleChanged() {
|
override fun notificationStyleChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun registerFirebasePusher(matrixClient: MatrixClient) {
|
override fun getAvailablePushProviders(): List<PushProvider> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun testPush() {
|
override suspend fun testPush() {
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,6 @@ koverMerged {
|
||||||
target = kotlinx.kover.api.VerificationTarget.CLASS
|
target = kotlinx.kover.api.VerificationTarget.CLASS
|
||||||
overrideClassFilter {
|
overrideClassFilter {
|
||||||
includes += "*Presenter"
|
includes += "*Presenter"
|
||||||
excludes += "*TemplatePresenter"
|
|
||||||
excludes += "*Fake*Presenter"
|
excludes += "*Fake*Presenter"
|
||||||
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
changelog.d/110.feature
Normal file
1
changelog.d/110.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[Create and join rooms] Create a room screen (UI)
|
||||||
1
changelog.d/300.feature
Normal file
1
changelog.d/300.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Implement room member details screen
|
||||||
|
|
@ -136,7 +136,7 @@ git clone git@github.com:matrix-org/matrix-rust-sdk.git
|
||||||
git clone git@github.com:matrix-org/matrix-rust-components-kotlin.git
|
git clone git@github.com:matrix-org/matrix-rust-components-kotlin.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can launch the build script with the following params:
|
Then you can launch the build script from the matrix-rust-components-kotlin repository with the following params:
|
||||||
|
|
||||||
- `-p` Local path to the rust-sdk repository
|
- `-p` Local path to the rust-sdk repository
|
||||||
- `-o` Optional output path with the expected name of the aar file. By default the aar will be located in the corresponding build/outputs/aar directory.
|
- `-o` Optional output path with the expected name of the aar file. By default the aar will be located in the corresponding build/outputs/aar directory.
|
||||||
|
|
@ -150,12 +150,12 @@ So for example to build the sdk against aarch64-linux-android target and copy th
|
||||||
./scripts/build.sh -p [YOUR MATRIX RUST SDK PATH] -t aarch64-linux-android -o [YOUR element-x-android PATH]/libraries/rustsdk/matrix-rust-sdk.aar
|
./scripts/build.sh -p [YOUR MATRIX RUST SDK PATH] -t aarch64-linux-android -o [YOUR element-x-android PATH]/libraries/rustsdk/matrix-rust-sdk.aar
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally let the `matrix/impl` module use this aar by switching those lines in the gradle file :
|
Finally let the `matrix/impl` module use this aar by changing the dependencies from `libs.matrix.sdk` to `projects.libraries.rustsdk`:
|
||||||
|
|
||||||
```groovy
|
```groovy
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.libraries.rustsdk) // <- comment this line
|
api(projects.libraries.rustsdk) // <- use the local version of the sdk. Uncomment this line.
|
||||||
// api(libs.matrix.sdk) // <- uncomment this line
|
//implementation(libs.matrix.sdk) // <- use the released version. Comment this line.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,14 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.features.userlist.api)
|
implementation(projects.features.userlist.api)
|
||||||
api(projects.features.createroom.api)
|
api(projects.features.createroom.api)
|
||||||
|
implementation(libs.coil.compose) // FIXME temp
|
||||||
|
|
||||||
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.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.impl)
|
testImplementation(projects.features.userlist.impl)
|
||||||
testImplementation(projects.features.userlist.test)
|
testImplementation(projects.features.userlist.test)
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl
|
package io.element.android.features.createroom.impl
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// TODO this is empty as we currently don't have an endpoint to perform user search
|
// TODO this is empty as we currently don't have an endpoint to perform user search
|
||||||
class AllMatrixUsersDataSource @Inject constructor() : MatrixUserDataSource {
|
class AllMatrixUsersDataSource @Inject constructor() : UserListDataSource {
|
||||||
override suspend fun search(query: String): List<MatrixUser> {
|
override suspend fun search(query: String): List<MatrixUser> {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.composable.Children
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode
|
||||||
|
import io.element.android.features.createroom.impl.di.CreateRoomComponent
|
||||||
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.DaggerComponentOwner
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class ConfigureRoomFlowNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
) : DaggerComponentOwner,
|
||||||
|
BackstackNode<ConfigureRoomFlowNode.NavTarget>(
|
||||||
|
backstack = BackStack(
|
||||||
|
initialElement = NavTarget.Root,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val component by lazy {
|
||||||
|
parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val daggerComponent: Any
|
||||||
|
get() = component
|
||||||
|
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
object Root : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object ConfigureRoom : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
NavTarget.Root -> {
|
||||||
|
val callback = object : AddPeopleNode.Callback {
|
||||||
|
override fun onContinue() {
|
||||||
|
backstack.push(NavTarget.ConfigureRoom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<AddPeopleNode>(context = buildContext, plugins = listOf(callback))
|
||||||
|
}
|
||||||
|
NavTarget.ConfigureRoom -> {
|
||||||
|
createNode<ConfigureRoomNode>(context = buildContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Children(
|
||||||
|
navModel = backstack,
|
||||||
|
modifier = modifier,
|
||||||
|
transitionHandler = rememberDefaultTransitionHandler()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl
|
||||||
|
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
data class CreateRoomConfig(
|
||||||
|
val roomName: String? = null,
|
||||||
|
val topic: String? = null,
|
||||||
|
val avatarUrl: String? = null,
|
||||||
|
val invites: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
|
val privacy: RoomPrivacy? = null,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl
|
||||||
|
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomPrivacy
|
||||||
|
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SingleIn(CreateRoomScope::class)
|
||||||
|
class CreateRoomDataStore @Inject constructor(
|
||||||
|
val selectedUserListDataStore: UserListDataStore,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
|
||||||
|
|
||||||
|
fun getCreateRoomConfig(): Flow<CreateRoomConfig> = combine(
|
||||||
|
selectedUserListDataStore.selectedUsers(),
|
||||||
|
createRoomConfigFlow,
|
||||||
|
) { selectedUsers, config ->
|
||||||
|
config.copy(invites = selectedUsers.toImmutableList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRoomName(roomName: String?) {
|
||||||
|
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(roomName = roomName?.takeIf { it.isNotEmpty() }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTopic(topic: String?) {
|
||||||
|
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(topic = topic?.takeIf { it.isNotEmpty() }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAvatarUrl(avatarUrl: String?) {
|
||||||
|
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(avatarUrl = avatarUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPrivacy(privacy: RoomPrivacy?) {
|
||||||
|
createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(privacy = privacy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,6 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.createroom.impl.addpeople.AddPeopleNode
|
|
||||||
import io.element.android.features.createroom.impl.root.CreateRoomRootNode
|
import io.element.android.features.createroom.impl.root.CreateRoomRootNode
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
|
|
@ -72,9 +71,11 @@ class CreateRoomFlowNode @AssistedInject constructor(
|
||||||
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createNode<CreateRoomRootNode>(buildContext, plugins = listOf(callback))
|
createNode<CreateRoomRootNode>(context = buildContext, plugins = listOf(callback))
|
||||||
|
}
|
||||||
|
NavTarget.NewRoom -> {
|
||||||
|
createNode<ConfigureRoomFlowNode>(context = buildContext, plugins = emptyList())
|
||||||
}
|
}
|
||||||
NavTarget.NewRoom -> createNode<AddPeopleNode>(buildContext)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,26 +21,35 @@ import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(CreateRoomScope::class)
|
||||||
class AddPeopleNode @AssistedInject constructor(
|
class AddPeopleNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
private val presenter: AddPeoplePresenter,
|
private val presenter: AddPeoplePresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onContinue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onContinue() {
|
||||||
|
plugins<Callback>().forEach { it.onContinue() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
AddPeopleView(
|
AddPeopleView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackPressed = { navigateUp() },
|
onBackPressed = this::navigateUp,
|
||||||
onNextPressed = { },
|
onNextPressed = this::onContinue,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,38 +17,33 @@
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.userlist.api.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
|
import io.element.android.features.userlist.api.UserListState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
|
|
||||||
class AddPeoplePresenter @Inject constructor(
|
class AddPeoplePresenter @Inject constructor(
|
||||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||||
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
@Named("AllUsers") private val userListDataSource: UserListDataSource,
|
||||||
) : Presenter<AddPeopleState> {
|
private val dataStore: CreateRoomDataStore,
|
||||||
|
) : Presenter<UserListState> {
|
||||||
|
|
||||||
private val userListPresenter by lazy {
|
private val userListPresenter by lazy {
|
||||||
userListPresenterFactory.create(
|
userListPresenterFactory.create(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||||
matrixUserDataSource,
|
userListDataSource,
|
||||||
|
dataStore.selectedUserListDataStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AddPeopleState {
|
override fun present(): UserListState {
|
||||||
val userListState = userListPresenter.present()
|
return userListPresenter.present()
|
||||||
|
|
||||||
fun handleEvents(event: AddPeopleEvents) {
|
|
||||||
// do nothing for now
|
|
||||||
}
|
|
||||||
|
|
||||||
return AddPeopleState(
|
|
||||||
userListState = userListState,
|
|
||||||
eventSink = ::handleEvents,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,30 +18,27 @@ package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
|
import io.element.android.features.userlist.api.UserListState
|
||||||
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
import io.element.android.features.userlist.api.aUserListState
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class AddPeopleStateProvider : PreviewParameterProvider<AddPeopleState> {
|
open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
override val values: Sequence<AddPeopleState>
|
override val values: Sequence<UserListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aAddPeopleState(),
|
aUserListState(),
|
||||||
aAddPeopleState().copy(
|
aUserListState().copy(
|
||||||
userListState = aUserListState().copy(
|
searchResults = aMatrixUserList().toImmutableList(),
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
selectionMode = SelectionMode.Multiple,
|
isSearchActive = false,
|
||||||
)
|
selectionMode = SelectionMode.Multiple,
|
||||||
),
|
),
|
||||||
aAddPeopleState().copy(
|
aUserListState().copy(
|
||||||
userListState = aUserListState().copy(
|
searchResults = aMatrixUserList().toImmutableList(),
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aAddPeopleState() = AddPeopleState(
|
|
||||||
userListState = aUserListState(),
|
|
||||||
eventSink = {}
|
|
||||||
)
|
|
||||||
|
|
@ -29,8 +29,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
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 androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.userlist.api.UserListView
|
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
|
import io.element.android.features.userlist.api.UserListState
|
||||||
|
import io.element.android.features.userlist.api.components.UserListView
|
||||||
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.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -43,18 +44,16 @@ import io.element.android.libraries.ui.strings.R as StringR
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AddPeopleView(
|
fun AddPeopleView(
|
||||||
state: AddPeopleState,
|
state: UserListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
onNextPressed: () -> Unit = {},
|
onNextPressed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val eventSink = state.eventSink
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!state.userListState.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
AddPeopleViewTopBar(
|
AddPeopleViewTopBar(
|
||||||
hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(),
|
hasSelectedUsers = state.selectedUsers.isNotEmpty(),
|
||||||
onBackPressed = onBackPressed,
|
onBackPressed = onBackPressed,
|
||||||
onNextPressed = onNextPressed,
|
onNextPressed = onNextPressed,
|
||||||
)
|
)
|
||||||
|
|
@ -68,7 +67,7 @@ fun AddPeopleView(
|
||||||
) {
|
) {
|
||||||
UserListView(
|
UserListView(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = state.userListState,
|
state = state,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,15 +108,15 @@ fun AddPeopleViewTopBar(
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AddPeopleViewLightPreview(@PreviewParameter(AddPeopleStateProvider::class) state: AddPeopleState) =
|
internal fun AddPeopleViewLightPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) =
|
||||||
ElementPreviewLight { ContentToPreview(state) }
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AddPeopleViewDarkPreview(@PreviewParameter(AddPeopleStateProvider::class) state: AddPeopleState) =
|
internal fun AddPeopleViewDarkPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) =
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: AddPeopleState) {
|
private fun ContentToPreview(state: UserListState) {
|
||||||
AddPeopleView(state = state)
|
AddPeopleView(state = state)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.components
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.AddAPhoto
|
||||||
|
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.clip
|
||||||
|
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
|
||||||
|
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Avatar(
|
||||||
|
avatarUri: Uri?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val commonModifier = modifier
|
||||||
|
.size(70.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
|
||||||
|
if (avatarUri != null) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val model = ImageRequest.Builder(context)
|
||||||
|
.data(avatarUri)
|
||||||
|
.build()
|
||||||
|
AsyncImage(
|
||||||
|
modifier = commonModifier,
|
||||||
|
model = model,
|
||||||
|
placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(modifier = commonModifier.background(LocalColors.current.quinary)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.AddAPhoto,
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(40.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AvatarLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AvatarDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
Row {
|
||||||
|
Avatar(null)
|
||||||
|
Avatar(Uri.EMPTY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.createroom.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LabelledTextField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
placeholder: String = "",
|
||||||
|
maxLines: Int = 1,
|
||||||
|
onValueChange: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
text = label
|
||||||
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = value,
|
||||||
|
placeholder = { Text(placeholder) },
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
maxLines = maxLines,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun LabelledTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun LabelledTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
Column {
|
||||||
|
LabelledTextField(
|
||||||
|
label = stringResource(R.string.screen_create_room_room_name_label),
|
||||||
|
value = "",
|
||||||
|
placeholder = stringResource(R.string.screen_create_room_room_name_placeholder),
|
||||||
|
)
|
||||||
|
LabelledTextField(
|
||||||
|
label = stringResource(R.string.screen_create_room_room_name_label),
|
||||||
|
value = "a room name",
|
||||||
|
placeholder = stringResource(R.string.screen_create_room_room_name_placeholder),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem
|
||||||
|
import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoomPrivacyOption(
|
||||||
|
roomPrivacyItem: RoomPrivacyItem,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
onOptionSelected: (RoomPrivacyItem) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.selectable(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = { onOptionSelected(roomPrivacyItem) },
|
||||||
|
role = Role.RadioButton,
|
||||||
|
)
|
||||||
|
.padding(8.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
|
imageVector = roomPrivacyItem.icon,
|
||||||
|
contentDescription = "",
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = roomPrivacyItem.title,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(3.dp))
|
||||||
|
Text(
|
||||||
|
text = roomPrivacyItem.description,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 17.sp,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
RadioButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.size(48.dp),
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = null // null recommended for accessibility with screenreaders
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RoomPrivacyOptionLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RoomPrivacyOptionDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
val aRoomPrivacyItem = roomPrivacyItems().first()
|
||||||
|
Column {
|
||||||
|
RoomPrivacyOption(
|
||||||
|
roomPrivacyItem = aRoomPrivacyItem,
|
||||||
|
isSelected = true,
|
||||||
|
)
|
||||||
|
RoomPrivacyOption(
|
||||||
|
roomPrivacyItem = aRoomPrivacyItem,
|
||||||
|
isSelected = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
|
sealed interface ConfigureRoomEvents {
|
||||||
|
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
||||||
|
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
||||||
|
data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents
|
||||||
|
data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents
|
||||||
|
data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents
|
||||||
|
object CreateRoom : ConfigureRoomEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.createroom.impl.di.CreateRoomScope
|
||||||
|
|
||||||
|
@ContributesNode(CreateRoomScope::class)
|
||||||
|
class ConfigureRoomNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: ConfigureRoomPresenter,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
ConfigureRoomView(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
onBackPressed = { navigateUp() } // TODO we should keep in memory the current view state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ConfigureRoomPresenter @Inject constructor(
|
||||||
|
private val dataStore: CreateRoomDataStore,
|
||||||
|
) : Presenter<ConfigureRoomState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): ConfigureRoomState {
|
||||||
|
val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig())
|
||||||
|
val isCreateButtonEnabled by remember(createRoomConfig.value.roomName, createRoomConfig.value.privacy) {
|
||||||
|
derivedStateOf {
|
||||||
|
createRoomConfig.value.roomName.isNullOrEmpty().not() && createRoomConfig.value.privacy != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: ConfigureRoomEvents) {
|
||||||
|
when (event) {
|
||||||
|
is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString())
|
||||||
|
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
||||||
|
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
||||||
|
is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy)
|
||||||
|
is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
|
||||||
|
ConfigureRoomEvents.CreateRoom -> Unit // TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConfigureRoomState(
|
||||||
|
createRoomConfig.value,
|
||||||
|
isCreateButtonEnabled = isCreateButtonEnabled,
|
||||||
|
eventSink = ::handleEvents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,11 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
data class AddPeopleState(
|
data class ConfigureRoomPresenterArgs(
|
||||||
val userListState: UserListState,
|
val selectedUsers: List<MatrixUser>,
|
||||||
val eventSink: (AddPeopleEvents) -> Unit,
|
|
||||||
)
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
|
|
||||||
|
data class ConfigureRoomState(
|
||||||
|
val config: CreateRoomConfig,
|
||||||
|
val isCreateButtonEnabled: Boolean,
|
||||||
|
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
|
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||||
|
|
||||||
|
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
|
override val values: Sequence<ConfigureRoomState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aConfigureRoomState(),
|
||||||
|
aConfigureRoomState().copy(
|
||||||
|
config = CreateRoomConfig(
|
||||||
|
roomName = "Room 101",
|
||||||
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines",
|
||||||
|
invites = aListOfSelectedUsers(),
|
||||||
|
privacy = RoomPrivacy.Private,
|
||||||
|
),
|
||||||
|
isCreateButtonEnabled = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aConfigureRoomState() = ConfigureRoomState(
|
||||||
|
config = CreateRoomConfig(),
|
||||||
|
isCreateButtonEnabled = false,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import io.element.android.features.createroom.impl.R
|
||||||
|
import io.element.android.features.createroom.impl.components.Avatar
|
||||||
|
import io.element.android.features.createroom.impl.components.LabelledTextField
|
||||||
|
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
||||||
|
import io.element.android.features.userlist.api.components.SelectedUsersList
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfigureRoomView(
|
||||||
|
state: ConfigureRoomState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackPressed: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
ConfigureRoomToolbar(
|
||||||
|
isNextActionEnabled = state.isCreateButtonEnabled,
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
|
onNextPressed = {
|
||||||
|
// state.eventSink(ConfigureRoomEvents.CreateRoom)
|
||||||
|
Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(padding),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
) {
|
||||||
|
RoomNameWithAvatar(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
avatarUri = state.config.avatarUrl?.toUri(),
|
||||||
|
roomName = state.config.roomName.orEmpty(),
|
||||||
|
onAvatarClick = { Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() },
|
||||||
|
onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
||||||
|
)
|
||||||
|
RoomTopic(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
topic = state.config.topic.orEmpty(),
|
||||||
|
onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
||||||
|
)
|
||||||
|
SelectedUsersList(
|
||||||
|
contentPadding = PaddingValues(horizontal = 24.dp),
|
||||||
|
selectedUsers = state.config.invites,
|
||||||
|
onUserRemoved = { state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it)) },
|
||||||
|
)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
RoomPrivacyOptions(
|
||||||
|
modifier = Modifier.padding(bottom = 40.dp),
|
||||||
|
selected = state.config.privacy,
|
||||||
|
onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfigureRoomToolbar(
|
||||||
|
isNextActionEnabled: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackPressed: () -> Unit = {},
|
||||||
|
onNextPressed: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_create_room_title),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = { BackButton(onClick = onBackPressed) },
|
||||||
|
actions = {
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
|
enabled = isNextActionEnabled,
|
||||||
|
onClick = onNextPressed,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(StringR.string.action_create),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoomNameWithAvatar(
|
||||||
|
avatarUri: Uri?,
|
||||||
|
roomName: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onAvatarClick: () -> Unit = {},
|
||||||
|
onRoomNameChanged: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Avatar(
|
||||||
|
avatarUri = avatarUri,
|
||||||
|
onClick = onAvatarClick,
|
||||||
|
)
|
||||||
|
|
||||||
|
LabelledTextField(
|
||||||
|
label = stringResource(R.string.screen_create_room_room_name_label),
|
||||||
|
value = roomName,
|
||||||
|
placeholder = stringResource(R.string.screen_create_room_room_name_placeholder),
|
||||||
|
onValueChange = onRoomNameChanged
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoomTopic(
|
||||||
|
topic: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onTopicChanged: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
LabelledTextField(
|
||||||
|
modifier = modifier,
|
||||||
|
label = stringResource(R.string.screen_create_room_topic_label),
|
||||||
|
value = topic,
|
||||||
|
placeholder = stringResource(R.string.screen_create_room_topic_placeholder),
|
||||||
|
onValueChange = onTopicChanged,
|
||||||
|
maxLines = 3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoomPrivacyOptions(
|
||||||
|
selected: RoomPrivacy?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onOptionSelected: (RoomPrivacyItem) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val items = roomPrivacyItems()
|
||||||
|
Column(modifier = modifier.selectableGroup()) {
|
||||||
|
items.forEach { item ->
|
||||||
|
RoomPrivacyOption(
|
||||||
|
roomPrivacyItem = item,
|
||||||
|
isSelected = selected == item.privacy,
|
||||||
|
onOptionSelected = onOptionSelected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
|
||||||
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: ConfigureRoomState) {
|
||||||
|
ConfigureRoomView(
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.configureroom
|
||||||
|
|
||||||
|
enum class RoomPrivacy {
|
||||||
|
Private,
|
||||||
|
Public,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Lock
|
||||||
|
import androidx.compose.material.icons.outlined.Public
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import io.element.android.features.createroom.impl.R
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
data class RoomPrivacyItem(
|
||||||
|
val privacy: RoomPrivacy,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun roomPrivacyItems(): ImmutableList<RoomPrivacyItem> {
|
||||||
|
return RoomPrivacy.values()
|
||||||
|
.map {
|
||||||
|
when (it) {
|
||||||
|
RoomPrivacy.Private -> RoomPrivacyItem(
|
||||||
|
privacy = it,
|
||||||
|
icon = Icons.Outlined.Lock,
|
||||||
|
title = stringResource(R.string.screen_create_room_private_option_title),
|
||||||
|
description = stringResource(R.string.screen_create_room_private_option_description),
|
||||||
|
)
|
||||||
|
RoomPrivacy.Public -> RoomPrivacyItem(
|
||||||
|
privacy = it,
|
||||||
|
icon = Icons.Outlined.Public,
|
||||||
|
title = stringResource(R.string.screen_create_room_public_option_title),
|
||||||
|
description = stringResource(R.string.screen_create_room_public_option_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toImmutableList()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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.createroom.impl.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import com.squareup.anvil.annotations.MergeSubcomponent
|
||||||
|
import dagger.Subcomponent
|
||||||
|
import io.element.android.libraries.architecture.NodeFactoriesBindings
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
|
||||||
|
@SingleIn(CreateRoomScope::class)
|
||||||
|
@MergeSubcomponent(CreateRoomScope::class)
|
||||||
|
interface CreateRoomComponent : NodeFactoriesBindings {
|
||||||
|
|
||||||
|
@Subcomponent.Builder
|
||||||
|
interface Builder {
|
||||||
|
fun build(): CreateRoomComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesTo(SessionScope::class)
|
||||||
|
interface ParentBindings {
|
||||||
|
fun createRoomComponentBuilder(): Builder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,7 @@ import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
import io.element.android.features.createroom.impl.AllMatrixUsersDataSource
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
|
|
||||||
|
|
@ -30,6 +30,6 @@ interface CreateRoomModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Named("AllUsers")
|
@Named("AllUsers")
|
||||||
fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): MatrixUserDataSource
|
fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): UserListDataSource
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.di
|
||||||
|
|
||||||
sealed interface AddPeopleEvents
|
abstract class CreateRoomScope private constructor()
|
||||||
|
|
@ -21,8 +21,9 @@ import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.userlist.api.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -38,14 +39,16 @@ import javax.inject.Named
|
||||||
|
|
||||||
class CreateRoomRootPresenter @Inject constructor(
|
class CreateRoomRootPresenter @Inject constructor(
|
||||||
private val presenterFactory: UserListPresenter.Factory,
|
private val presenterFactory: UserListPresenter.Factory,
|
||||||
@Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource,
|
@Named("AllUsers") private val userListDataSource: UserListDataSource,
|
||||||
|
private val userListDataStore: UserListDataStore,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
) : Presenter<CreateRoomRootState> {
|
) : Presenter<CreateRoomRootState> {
|
||||||
|
|
||||||
private val presenter by lazy {
|
private val presenter by lazy {
|
||||||
presenterFactory.create(
|
presenterFactory.create(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
matrixUserDataSource,
|
userListDataSource,
|
||||||
|
userListDataStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.userlist.api.UserListView
|
import io.element.android.features.userlist.api.components.UserListView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
|
|
@ -110,7 +110,7 @@ fun CreateRoomRootView(
|
||||||
}
|
}
|
||||||
is Async.Failure -> {
|
is Async.Failure -> {
|
||||||
RetryDialog(
|
RetryDialog(
|
||||||
content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat),
|
content = stringResource(id = R.string.screen_start_chat_error_starting_chat),
|
||||||
onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
|
onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) },
|
||||||
onRetry = {
|
onRetry = {
|
||||||
state.userListState.selectedUsers.firstOrNull()
|
state.userListState.selectedUsers.firstOrNull()
|
||||||
|
|
@ -156,7 +156,7 @@ fun CreateRoomActionButtonsList(
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
CreateRoomActionButton(
|
CreateRoomActionButton(
|
||||||
iconRes = DrawableR.drawable.ic_groups,
|
iconRes = DrawableR.drawable.ic_groups,
|
||||||
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
text = stringResource(id = StringR.string.action_create_a_room),
|
||||||
onClick = onNewRoomClicked,
|
onClick = onNewRoomClicked,
|
||||||
)
|
)
|
||||||
CreateRoomActionButton(
|
CreateRoomActionButton(
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,6 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nueva sala"</string>
|
<string name="screen_create_room_action_create_room">"Nueva sala"</string>
|
||||||
<string name="screen_create_room_action_invite_people">"Invitar gente"</string>
|
<string name="screen_create_room_action_invite_people">"Invitar gente"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Añadir personas"</string>
|
<string name="screen_create_room_add_people_title">"Añadir personas"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
|
||||||
|
<string name="screen_start_chat_unknown_profile">"No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -3,4 +3,6 @@
|
||||||
<string name="screen_create_room_action_create_room">"Nuova stanza"</string>
|
<string name="screen_create_room_action_create_room">"Nuova stanza"</string>
|
||||||
<string name="screen_create_room_action_invite_people">"Invita persone"</string>
|
<string name="screen_create_room_action_invite_people">"Invita persone"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Aggiungi persone"</string>
|
<string name="screen_create_room_add_people_title">"Aggiungi persone"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
|
||||||
|
<string name="screen_start_chat_unknown_profile">"Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -3,4 +3,6 @@
|
||||||
<string name="screen_create_room_action_create_room">"Cameră nouă"</string>
|
<string name="screen_create_room_action_create_room">"Cameră nouă"</string>
|
||||||
<string name="screen_create_room_action_invite_people">"Invitați persoane"</string>
|
<string name="screen_create_room_action_invite_people">"Invitați persoane"</string>
|
||||||
<string name="screen_create_room_add_people_title">"Adaugați persoane"</string>
|
<string name="screen_create_room_add_people_title">"Adaugați persoane"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
|
||||||
|
<string name="screen_start_chat_unknown_profile">"Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -12,4 +12,6 @@
|
||||||
<string name="screen_create_room_title">"Create a room"</string>
|
<string name="screen_create_room_title">"Create a room"</string>
|
||||||
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"What is this room about?"</string>
|
<string name="screen_create_room_topic_placeholder">"What is this room about?"</string>
|
||||||
|
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
|
||||||
|
<string name="screen_start_chat_unknown_profile">"We can’t validate this user’s Matrix ID. The invite might not be received."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -22,7 +22,9 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
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.userlist.test.FakeMatrixUserDataSource
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
|
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -35,7 +37,7 @@ class AddPeoplePresenterTests {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeMatrixUserDataSource())
|
presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource(), CreateRoomDataStore(UserListDataStore()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||||
|
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class ConfigureRoomPresenterTests {
|
||||||
|
|
||||||
|
private lateinit var presenter: ConfigureRoomPresenter
|
||||||
|
private lateinit var userListDataStore: UserListDataStore
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
userListDataStore = UserListDataStore()
|
||||||
|
presenter = ConfigureRoomPresenter(CreateRoomDataStore(userListDataStore))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.config).isEqualTo(CreateRoomConfig())
|
||||||
|
assertThat(initialState.config.roomName).isNull()
|
||||||
|
assertThat(initialState.config.topic).isNull()
|
||||||
|
assertThat(initialState.config.invites).isEmpty()
|
||||||
|
assertThat(initialState.config.avatarUrl).isNull()
|
||||||
|
assertThat(initialState.config.privacy).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - create room button is enabled only if the required fields are completed`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
var config = initialState.config
|
||||||
|
assertThat(initialState.isCreateButtonEnabled).isFalse()
|
||||||
|
|
||||||
|
// Room name not empty
|
||||||
|
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
|
||||||
|
var newState: ConfigureRoomState = awaitItem()
|
||||||
|
config = config.copy(roomName = A_ROOM_NAME)
|
||||||
|
assertThat(newState.config).isEqualTo(config)
|
||||||
|
assertThat(newState.isCreateButtonEnabled).isFalse()
|
||||||
|
|
||||||
|
// Select privacy
|
||||||
|
newState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Private))
|
||||||
|
newState = awaitItem()
|
||||||
|
config = config.copy(privacy = RoomPrivacy.Private)
|
||||||
|
assertThat(newState.config).isEqualTo(config)
|
||||||
|
assertThat(newState.isCreateButtonEnabled).isTrue()
|
||||||
|
|
||||||
|
// Clear room name
|
||||||
|
newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
|
||||||
|
newState = awaitItem()
|
||||||
|
config = config.copy(roomName = null)
|
||||||
|
assertThat(newState.config).isEqualTo(config)
|
||||||
|
assertThat(newState.isCreateButtonEnabled).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - state is updated when fields are changed`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
var expectedConfig = CreateRoomConfig()
|
||||||
|
assertThat(initialState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
|
// Select User
|
||||||
|
val selectedUser1 = aMatrixUser()
|
||||||
|
val selectedUser2 = aMatrixUser("@id_of_bob:server.org", "Bob")
|
||||||
|
userListDataStore.selectUser(selectedUser1)
|
||||||
|
skipItems(1)
|
||||||
|
userListDataStore.selectUser(selectedUser2)
|
||||||
|
var newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(invites = persistentListOf(selectedUser1, selectedUser2))
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
|
// Room name
|
||||||
|
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
|
||||||
|
newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
|
// Room topic
|
||||||
|
newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE))
|
||||||
|
newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(topic = A_MESSAGE)
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
|
// Room avatar
|
||||||
|
val anUri = Uri.parse(AN_AVATAR_URL)
|
||||||
|
newState.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri))
|
||||||
|
newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(avatarUrl = anUri.toString())
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
|
// Room privacy
|
||||||
|
newState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public))
|
||||||
|
newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(privacy = RoomPrivacy.Public)
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
|
// Remove user
|
||||||
|
newState.eventSink(ConfigureRoomEvents.RemoveFromSelection(selectedUser1))
|
||||||
|
newState = awaitItem()
|
||||||
|
expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList())
|
||||||
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -22,8 +22,9 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
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.userlist.api.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
import io.element.android.features.userlist.api.aUserListState
|
||||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenter
|
import io.element.android.features.userlist.test.FakeUserListPresenter
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -41,7 +42,7 @@ import org.junit.Test
|
||||||
|
|
||||||
class CreateRoomRootPresenterTests {
|
class CreateRoomRootPresenterTests {
|
||||||
|
|
||||||
private lateinit var userListDataSource: FakeMatrixUserDataSource
|
private lateinit var userListDataSource: FakeUserListDataSource
|
||||||
private lateinit var presenter: CreateRoomRootPresenter
|
private lateinit var presenter: CreateRoomRootPresenter
|
||||||
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
private lateinit var fakeUserListPresenter: FakeUserListPresenter
|
||||||
private lateinit var fakeMatrixClient: FakeMatrixClient
|
private lateinit var fakeMatrixClient: FakeMatrixClient
|
||||||
|
|
@ -50,8 +51,8 @@ class CreateRoomRootPresenterTests {
|
||||||
fun setup() {
|
fun setup() {
|
||||||
fakeUserListPresenter = FakeUserListPresenter()
|
fakeUserListPresenter = FakeUserListPresenter()
|
||||||
fakeMatrixClient = FakeMatrixClient()
|
fakeMatrixClient = FakeMatrixClient()
|
||||||
userListDataSource = FakeMatrixUserDataSource()
|
userListDataSource = FakeUserListDataSource()
|
||||||
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, fakeMatrixClient)
|
presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, UserListDataStore(), fakeMatrixClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
@ -54,6 +56,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object RoomMemberList : NavTarget
|
object RoomMemberList : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class RoomMemberDetails(val roomMember: RoomMember) : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -66,7 +71,18 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
createNode<RoomDetailsNode>(buildContext, listOf(callback))
|
createNode<RoomDetailsNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.RoomMemberList -> createNode<RoomMemberListNode>(buildContext)
|
NavTarget.RoomMemberList -> {
|
||||||
|
val callback = object : RoomMemberListNode.Callback {
|
||||||
|
override fun openRoomMemberDetails(roomMember: RoomMember) {
|
||||||
|
backstack.push(NavTarget.RoomMemberDetails(roomMember))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<RoomMemberListNode>(buildContext, listOf(callback))
|
||||||
|
}
|
||||||
|
is NavTarget.RoomMemberDetails -> {
|
||||||
|
val inputs = RoomMemberDetailsNode.Inputs(navTarget.roomMember)
|
||||||
|
createNode<RoomMemberDetailsNode>(buildContext, listOf(inputs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class RoomDetailsNode @AssistedInject constructor(
|
class RoomDetailsNode @AssistedInject constructor(
|
||||||
|
|
@ -61,7 +60,6 @@ class RoomDetailsNode @AssistedInject constructor(
|
||||||
activityResultLauncher = null,
|
activityResultLauncher = null,
|
||||||
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||||
text = permalink,
|
text = permalink,
|
||||||
noActivityFoundMessage = context.getString(StringR.string.error_no_compatible_app_found)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ fun RoomDetailsView(
|
||||||
roomAlias = state.roomAlias
|
roomAlias = state.roomAlias
|
||||||
)
|
)
|
||||||
|
|
||||||
ShareSection(onShareRoom = onShareRoom)
|
ShareSection(onShareUser = onShareRoom)
|
||||||
|
|
||||||
if (state.roomTopic != null) {
|
if (state.roomTopic != null) {
|
||||||
TopicSection(roomTopic = state.roomTopic)
|
TopicSection(roomTopic = state.roomTopic)
|
||||||
|
|
@ -127,12 +127,12 @@ fun RoomDetailsView(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ShareSection(onShareRoom: () -> Unit, modifier: Modifier = Modifier) {
|
internal fun ShareSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
PreferenceCategory(modifier = modifier) {
|
PreferenceCategory(modifier = modifier) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = stringResource(R.string.screen_room_details_share_room_title),
|
title = stringResource(R.string.screen_room_details_share_room_title),
|
||||||
icon = Icons.Outlined.Share,
|
icon = Icons.Outlined.Share,
|
||||||
onClick = onShareRoom,
|
onClick = onShareUser,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +172,6 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
|
||||||
color = MaterialTheme.colorScheme.tertiary
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.impl.di
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(RoomScope::class)
|
||||||
|
interface RoomMemberBindsModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Named("RoomMembers")
|
||||||
|
fun bindRoomMemberUserListDataSource(dataSource: RoomUserListDataSource): UserListDataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@ContributesTo(RoomScope::class)
|
||||||
|
object RoomMemberProvidesModule {
|
||||||
|
@Provides
|
||||||
|
fun provideRoomMemberDetailsPresenterFactory(
|
||||||
|
room: MatrixRoom,
|
||||||
|
): RoomMemberDetailsPresenter.Factory {
|
||||||
|
return object : RoomMemberDetailsPresenter.Factory {
|
||||||
|
override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter {
|
||||||
|
return RoomMemberDetailsPresenter(room, roomMember)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,10 +21,14 @@ import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.roomdetails.impl.RoomDetailsFlowNode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -32,11 +36,23 @@ import timber.log.Timber
|
||||||
class RoomMemberListNode @AssistedInject constructor(
|
class RoomMemberListNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val room: MatrixRoom,
|
||||||
private val presenter: RoomMemberListPresenter,
|
private val presenter: RoomMemberListPresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun openRoomMemberDetails(roomMember: RoomMember)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callback = plugins<Callback>().first()
|
||||||
|
|
||||||
private fun onUserSelected(matrixUser: MatrixUser) {
|
private fun onUserSelected(matrixUser: MatrixUser) {
|
||||||
Timber.d("TODO: implement user selection. User: $matrixUser")
|
val member = room.getMember(matrixUser.id)
|
||||||
|
if (member != null) {
|
||||||
|
callback.openRoomMemberDetails(member)
|
||||||
|
} else {
|
||||||
|
Timber.e("Could find room member ${matrixUser.id} in room ${room.roomId}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.userlist.api.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -36,13 +37,15 @@ import javax.inject.Named
|
||||||
|
|
||||||
class RoomMemberListPresenter @Inject constructor(
|
class RoomMemberListPresenter @Inject constructor(
|
||||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||||
@Named("RoomMembers") private val matrixUserDataSource: MatrixUserDataSource,
|
@Named("RoomMembers") private val userListDataSource: UserListDataSource,
|
||||||
|
private val userListDataStore: UserListDataStore,
|
||||||
) : Presenter<RoomMemberListState> {
|
) : Presenter<RoomMemberListState> {
|
||||||
|
|
||||||
private val userListPresenter by lazy {
|
private val userListPresenter by lazy {
|
||||||
userListPresenterFactory.create(
|
userListPresenterFactory.create(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
matrixUserDataSource,
|
userListDataSource,
|
||||||
|
userListDataStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +55,7 @@ class RoomMemberListPresenter @Inject constructor(
|
||||||
val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) }
|
val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
allUsers.value = Async.Success(matrixUserDataSource.search("").toImmutableList())
|
allUsers.value = Async.Success(userListDataSource.search("").toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RoomMemberListState(
|
return RoomMemberListState(
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.features.userlist.api.SearchSingleUserResultItem
|
import io.element.android.features.userlist.api.components.SearchSingleUserResultItem
|
||||||
import io.element.android.features.userlist.api.UserListView
|
import io.element.android.features.userlist.api.components.UserListView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.isLoading
|
import io.element.android.libraries.architecture.isLoading
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -25,9 +25,9 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomMatrixUserDataSource @Inject constructor(
|
class RoomUserListDataSource @Inject constructor(
|
||||||
private val room: MatrixRoom
|
private val room: MatrixRoom
|
||||||
) : MatrixUserDataSource {
|
) : UserListDataSource {
|
||||||
|
|
||||||
override suspend fun search(query: String): List<MatrixUser> {
|
override suspend fun search(query: String): List<MatrixUser> {
|
||||||
return room.members().filter { member ->
|
return room.members().filter { member ->
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.impl.members.details
|
||||||
|
|
||||||
|
// TODO Add your events or remove the file completely if no events
|
||||||
|
sealed interface RoomMemberDetailsEvents {
|
||||||
|
object MyEvent : RoomMemberDetailsEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.impl.members.details
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.roomdetails.impl.R
|
||||||
|
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import timber.log.Timber
|
||||||
|
import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
||||||
|
|
||||||
|
@ContributesNode(RoomScope::class)
|
||||||
|
class RoomMemberDetailsNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
presenterFactory: RoomMemberDetailsPresenter.Factory,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
data class Inputs(
|
||||||
|
val member: RoomMember,
|
||||||
|
) : NodeInputs
|
||||||
|
|
||||||
|
private val inputs = inputs<Inputs>()
|
||||||
|
private val presenter = presenterFactory.create(inputs.member)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
fun onShareUser() {
|
||||||
|
val permalinkResult = PermalinkBuilder.permalinkForUser(UserId(inputs.member.userId))
|
||||||
|
permalinkResult.onSuccess { permalink ->
|
||||||
|
startSharePlainTextIntent(
|
||||||
|
context = context,
|
||||||
|
activityResultLauncher = null,
|
||||||
|
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
|
||||||
|
text = permalink,
|
||||||
|
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val state = presenter.present()
|
||||||
|
RoomMemberDetailsView(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
goBack = this::navigateUp,
|
||||||
|
onShareUser = ::onShareUser
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.impl.members.details
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
||||||
|
class RoomMemberDetailsPresenter @AssistedInject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
@Assisted private val roomMember: RoomMember,
|
||||||
|
) : Presenter<RoomMemberDetailsState> {
|
||||||
|
|
||||||
|
interface Factory {
|
||||||
|
fun create(roomMember: RoomMember): RoomMemberDetailsPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): RoomMemberDetailsState {
|
||||||
|
|
||||||
|
// fun handleEvents(event: RoomMemberDetailsEvents) {
|
||||||
|
// when (event) {
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return RoomMemberDetailsState(
|
||||||
|
userId = roomMember.userId,
|
||||||
|
userName = roomMember.displayName,
|
||||||
|
avatarUrl = roomMember.avatarUrl,
|
||||||
|
isBlocked = roomMember.isIgnored,
|
||||||
|
// eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.impl.members.details
|
||||||
|
|
||||||
|
data class RoomMemberDetailsState(
|
||||||
|
val userId: String,
|
||||||
|
val userName: String?,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val isBlocked: Boolean,
|
||||||
|
// val eventSink: (RoomMemberDetailsEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.impl.members.details
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
|
open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> {
|
||||||
|
override val values: Sequence<RoomMemberDetailsState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aRoomMemberDetailsState(),
|
||||||
|
aRoomMemberDetailsState().copy(userName = null),
|
||||||
|
aRoomMemberDetailsState().copy(isBlocked = true),
|
||||||
|
// Add other states here
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aRoomMemberDetailsState() = RoomMemberDetailsState(
|
||||||
|
userId = "@daniel:domain.com",
|
||||||
|
userName = "Daniel",
|
||||||
|
avatarUrl = null,
|
||||||
|
isBlocked = false,
|
||||||
|
// eventSink = {},
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.impl.members.details
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Block
|
||||||
|
import androidx.compose.material.icons.outlined.ChatBubbleOutline
|
||||||
|
import androidx.compose.material.icons.outlined.Share
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.roomdetails.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
|
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.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RoomMemberDetailsView(
|
||||||
|
state: RoomMemberDetailsState,
|
||||||
|
onShareUser: () -> Unit,
|
||||||
|
goBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(title = { }, navigationIcon = { BackButton(onClick = goBack) })
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
HeaderSection(
|
||||||
|
avatarUrl = state.avatarUrl,
|
||||||
|
userId = state.userId,
|
||||||
|
userName = state.userName,
|
||||||
|
)
|
||||||
|
|
||||||
|
ShareSection(onShareUser = onShareUser)
|
||||||
|
|
||||||
|
SendMessageSection(onSendMessage = {
|
||||||
|
// TODO implement send DM
|
||||||
|
})
|
||||||
|
|
||||||
|
BlockSection(isBlocked = state.isBlocked, onToggleBlock = {
|
||||||
|
// TODO implement block & unblock
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun HeaderSection(
|
||||||
|
avatarUrl: String?,
|
||||||
|
userId: String,
|
||||||
|
userName: String?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Box(modifier = Modifier.size(70.dp)) {
|
||||||
|
Avatar(
|
||||||
|
avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.HUGE),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(30.dp))
|
||||||
|
if (userName != null) {
|
||||||
|
Text(userName, style = ElementTextStyles.Bold.title1)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
Text(userId, style = ElementTextStyles.Regular.body, color = MaterialTheme.colorScheme.secondary)
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ShareSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
PreferenceCategory(modifier = modifier) {
|
||||||
|
PreferenceText(
|
||||||
|
title = stringResource(StringR.string.action_share),
|
||||||
|
icon = Icons.Outlined.Share,
|
||||||
|
onClick = onShareUser,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
PreferenceCategory(modifier = modifier) {
|
||||||
|
PreferenceText(
|
||||||
|
title = stringResource(StringR.string.action_send_message),
|
||||||
|
icon = Icons.Outlined.ChatBubbleOutline,
|
||||||
|
onClick = onSendMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
PreferenceCategory(showDivider = false, modifier = modifier) {
|
||||||
|
if (isBlocked) {
|
||||||
|
PreferenceText(
|
||||||
|
title = stringResource(R.string.screen_dm_details_unblock_user),
|
||||||
|
icon = Icons.Outlined.Block,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PreferenceText(
|
||||||
|
title = stringResource(R.string.screen_dm_details_block_user),
|
||||||
|
icon = Icons.Outlined.Block,
|
||||||
|
tintColor = LocalColors.current.textActionCritical,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
|
||||||
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RoomMemberDetailsViewDarkPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: RoomMemberDetailsState) {
|
||||||
|
RoomMemberDetailsView(
|
||||||
|
state = state,
|
||||||
|
onShareUser = {},
|
||||||
|
goBack = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -232,7 +232,8 @@ fun aRoomMember(
|
||||||
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
||||||
isNameAmbiguous: Boolean = false,
|
isNameAmbiguous: Boolean = false,
|
||||||
powerLevel: Long = 0L,
|
powerLevel: Long = 0L,
|
||||||
normalizedPowerLevel: Long = 0L
|
normalizedPowerLevel: Long = 0L,
|
||||||
|
isIgnored: Boolean = false,
|
||||||
) = RoomMember(
|
) = RoomMember(
|
||||||
userId = userId.value,
|
userId = userId.value,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
|
|
@ -241,4 +242,5 @@ fun aRoomMember(
|
||||||
isNameAmbiguous = isNameAmbiguous,
|
isNameAmbiguous = isNameAmbiguous,
|
||||||
powerLevel = powerLevel,
|
powerLevel = powerLevel,
|
||||||
normalizedPowerLevel = normalizedPowerLevel,
|
normalizedPowerLevel = normalizedPowerLevel,
|
||||||
|
isIgnored = isIgnored,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,29 +22,37 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.userlist.api.SelectionMode
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.userlist.api.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.internal.toImmutableList
|
import okhttp3.internal.toImmutableList
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
class RoomMemberListPresenterTests {
|
class RoomMemberListPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - search is done automatically on start, but is async`() = runTest {
|
fun `present - search is done automatically on start, but is async`() = runTest {
|
||||||
val searchResult = listOf(aMatrixUser())
|
val searchResult = listOf(aMatrixUser())
|
||||||
val userListDataSource = FakeMatrixUserDataSource().apply {
|
val userListDataSource = FakeUserListDataSource().apply {
|
||||||
givenSearchResult(searchResult)
|
givenSearchResult(searchResult)
|
||||||
}
|
}
|
||||||
|
val userListDataStore = UserListDataStore()
|
||||||
val userListFactory = object : UserListPresenter.Factory {
|
val userListFactory = object : UserListPresenter.Factory {
|
||||||
override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource)
|
override fun create(
|
||||||
|
args: UserListPresenterArgs,
|
||||||
|
userListDataSource: UserListDataSource,
|
||||||
|
userListDataStore: UserListDataStore,
|
||||||
|
) = DefaultUserListPresenter(args, userListDataSource, userListDataStore)
|
||||||
}
|
}
|
||||||
val presenter = RoomMemberListPresenter(userListFactory, userListDataSource)
|
val presenter = RoomMemberListPresenter(userListFactory, userListDataSource, userListDataStore)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -58,5 +66,4 @@ class RoomMemberListPresenterTests {
|
||||||
Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList())
|
Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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.roomdetails.members.details
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
|
import io.element.android.features.roomdetails.aRoomMember
|
||||||
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class RoomMemberDetailsPresenterTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - returns the room member's data`() = runTest {
|
||||||
|
val room = aMatrixRoom()
|
||||||
|
val roomMember = aRoomMember(displayName = "Alice")
|
||||||
|
val presenter = RoomMemberDetailsPresenter(room, roomMember)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
Truth.assertThat(initialState.userId).isEqualTo(roomMember.userId)
|
||||||
|
Truth.assertThat(initialState.userName).isEqualTo(roomMember.displayName)
|
||||||
|
Truth.assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
|
||||||
|
Truth.assertThat(initialState.isBlocked).isEqualTo(roomMember.isIgnored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.userlist.api
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
interface MatrixUserDataSource {
|
interface UserListDataSource {
|
||||||
suspend fun search(query: String): List<MatrixUser>
|
suspend fun search(query: String): List<MatrixUser>
|
||||||
suspend fun getProfile(userId: UserId): MatrixUser?
|
suspend fun getProfile(userId: UserId): MatrixUser?
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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.userlist.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class UserListDataStore @Inject constructor() {
|
||||||
|
|
||||||
|
private val selectedUsers: MutableStateFlow<List<MatrixUser>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
fun selectUser(user: MatrixUser) {
|
||||||
|
if (user !in selectedUsers.value) {
|
||||||
|
selectedUsers.tryEmit(selectedUsers.value.plus(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUserFromSelection(user: MatrixUser) {
|
||||||
|
selectedUsers.tryEmit(selectedUsers.value.minus(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectedUsers(): Flow<List<MatrixUser>> = selectedUsers
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,10 @@ import io.element.android.libraries.architecture.Presenter
|
||||||
interface UserListPresenter : Presenter<UserListState> {
|
interface UserListPresenter : Presenter<UserListState> {
|
||||||
|
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter
|
fun create(
|
||||||
|
args: UserListPresenterArgs,
|
||||||
|
userListDataSource: UserListDataSource,
|
||||||
|
userListDataStore: UserListDataStore,
|
||||||
|
): UserListPresenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.userlist.api
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -24,7 +23,6 @@ data class UserListState(
|
||||||
val searchQuery: String,
|
val searchQuery: String,
|
||||||
val searchResults: ImmutableList<MatrixUser>,
|
val searchResults: ImmutableList<MatrixUser>,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
val selectedUsersListState: LazyListState,
|
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val selectionMode: SelectionMode,
|
val selectionMode: SelectionMode,
|
||||||
val eventSink: (UserListEvents) -> Unit,
|
val eventSink: (UserListEvents) -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,10 @@
|
||||||
|
|
||||||
package io.element.android.features.userlist.api
|
package io.element.android.features.userlist.api
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
override val values: Sequence<UserListState>
|
override val values: Sequence<UserListState>
|
||||||
|
|
@ -38,14 +37,14 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
searchResults = aListOfResults(),
|
searchResults = aMatrixUserList().toImmutableList(),
|
||||||
),
|
),
|
||||||
aUserListState().copy(
|
aUserListState().copy(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
searchQuery = "@someone:matrix.org",
|
searchQuery = "@someone:matrix.org",
|
||||||
selectionMode = SelectionMode.Multiple,
|
selectionMode = SelectionMode.Multiple,
|
||||||
selectedUsers = aListOfSelectedUsers(),
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
searchResults = aListOfResults(),
|
searchResults = aMatrixUserList().toImmutableList(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -55,33 +54,8 @@ fun aUserListState() = UserListState(
|
||||||
searchQuery = "",
|
searchQuery = "",
|
||||||
searchResults = persistentListOf(),
|
searchResults = persistentListOf(),
|
||||||
selectedUsers = persistentListOf(),
|
selectedUsers = persistentListOf(),
|
||||||
selectedUsersListState = LazyListState(
|
|
||||||
firstVisibleItemIndex = 0,
|
|
||||||
firstVisibleItemScrollOffset = 0,
|
|
||||||
),
|
|
||||||
selectionMode = SelectionMode.Single,
|
selectionMode = SelectionMode.Single,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aListOfSelectedUsers() = persistentListOf(
|
fun aListOfSelectedUsers() = aMatrixUserList().take(6).toImmutableList()
|
||||||
MatrixUser(id = UserId("@someone:matrix.org")),
|
|
||||||
MatrixUser(id = UserId("@other:matrix.org"), username = "other"),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aListOfResults() = persistentListOf(
|
|
||||||
MatrixUser(id = UserId("@someone:matrix.org")),
|
|
||||||
MatrixUser(id = UserId("@other:matrix.org"), username = "other"),
|
|
||||||
MatrixUser(
|
|
||||||
id = UserId("@someone_with_a_very_long_matrix_identifier:a_very_long_domain.org"),
|
|
||||||
username = "hey, I am someone with a very long display name"
|
|
||||||
),
|
|
||||||
MatrixUser(id = UserId("@someone_2:matrix.org"), username = "someone 2"),
|
|
||||||
MatrixUser(id = UserId("@someone_3:matrix.org"), username = "someone 3"),
|
|
||||||
MatrixUser(id = UserId("@someone_4:matrix.org"), username = "someone 4"),
|
|
||||||
MatrixUser(id = UserId("@someone_5:matrix.org"), username = "someone 5"),
|
|
||||||
MatrixUser(id = UserId("@someone_6:matrix.org"), username = "someone 6"),
|
|
||||||
MatrixUser(id = UserId("@someone_7:matrix.org"), username = "someone 7"),
|
|
||||||
MatrixUser(id = UserId("@someone_8:matrix.org"), username = "someone 8"),
|
|
||||||
MatrixUser(id = UserId("@someone_9:matrix.org"), username = "someone 9"),
|
|
||||||
MatrixUser(id = UserId("@someone_10:matrix.org"), username = "someone 10"),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,311 +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.userlist.api
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
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.button.BackButton
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
|
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
|
||||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UserListView(
|
|
||||||
state: UserListState,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onUserSelected: (MatrixUser) -> Unit = {},
|
|
||||||
onUserDeselected: (MatrixUser) -> Unit = {},
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
SearchUserBar(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
query = state.searchQuery,
|
|
||||||
results = state.searchResults,
|
|
||||||
selectedUsers = state.selectedUsers,
|
|
||||||
selectedUsersListState = state.selectedUsersListState,
|
|
||||||
active = state.isSearchActive,
|
|
||||||
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
|
||||||
onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
|
|
||||||
onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
|
|
||||||
onUserSelected = {
|
|
||||||
state.eventSink(UserListEvents.AddToSelection(it))
|
|
||||||
onUserSelected(it)
|
|
||||||
},
|
|
||||||
onUserDeselected = {
|
|
||||||
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
|
||||||
onUserDeselected(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) {
|
|
||||||
SelectedUsersList(
|
|
||||||
listState = state.selectedUsersListState,
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
selectedUsers = state.selectedUsers,
|
|
||||||
onUserRemoved = {
|
|
||||||
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
|
||||||
onUserDeselected(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun SearchUserBar(
|
|
||||||
query: String,
|
|
||||||
results: ImmutableList<MatrixUser>,
|
|
||||||
selectedUsers: ImmutableList<MatrixUser>,
|
|
||||||
selectedUsersListState: LazyListState,
|
|
||||||
active: Boolean,
|
|
||||||
isMultiSelectionEnabled: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
placeHolderTitle: String = stringResource(StringR.string.common_search_for_someone),
|
|
||||||
onActiveChanged: (Boolean) -> Unit = {},
|
|
||||||
onTextChanged: (String) -> Unit = {},
|
|
||||||
onUserSelected: (MatrixUser) -> Unit = {},
|
|
||||||
onUserDeselected: (MatrixUser) -> Unit = {},
|
|
||||||
) {
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
if (!active) {
|
|
||||||
onTextChanged("")
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchBar(
|
|
||||||
query = query,
|
|
||||||
onQueryChange = onTextChanged,
|
|
||||||
onSearch = { focusManager.clearFocus() },
|
|
||||||
active = active,
|
|
||||||
onActiveChange = onActiveChanged,
|
|
||||||
modifier = modifier
|
|
||||||
.padding(horizontal = if (!active) 16.dp else 0.dp),
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = placeHolderTitle,
|
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingIcon = if (active) {
|
|
||||||
{ BackButton(onClick = { onActiveChanged(false) }) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
trailingIcon = when {
|
|
||||||
active && query.isNotEmpty() -> {
|
|
||||||
{
|
|
||||||
IconButton(onClick = { onTextChanged("") }) {
|
|
||||||
Icon(Icons.Default.Close, stringResource(StringR.string.action_clear))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
!active -> {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Search,
|
|
||||||
contentDescription = stringResource(StringR.string.action_search),
|
|
||||||
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
|
||||||
content = {
|
|
||||||
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
|
|
||||||
SelectedUsersList(
|
|
||||||
listState = selectedUsersListState,
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
selectedUsers = selectedUsers,
|
|
||||||
onUserRemoved = onUserDeselected,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn {
|
|
||||||
if (isMultiSelectionEnabled) {
|
|
||||||
items(results) { matrixUser ->
|
|
||||||
SearchMultipleUsersResultItem(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null,
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
if (checked) {
|
|
||||||
onUserSelected(matrixUser)
|
|
||||||
} else {
|
|
||||||
onUserDeselected(matrixUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items(results) { matrixUser ->
|
|
||||||
SearchSingleUserResultItem(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
onClick = { onUserSelected(matrixUser) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SearchMultipleUsersResultItem(
|
|
||||||
matrixUser: MatrixUser,
|
|
||||||
isUserSelected: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
CheckableMatrixUserRow(
|
|
||||||
checked = isUserSelected,
|
|
||||||
modifier = modifier,
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
avatarSize = AvatarSize.Custom(36.dp),
|
|
||||||
onCheckedChange = onCheckedChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SearchSingleUserResultItem(
|
|
||||||
matrixUser: MatrixUser,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
MatrixUserRow(
|
|
||||||
modifier = modifier.clickable(onClick = onClick),
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
avatarSize = AvatarSize.Custom(36.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SelectedUsersList(
|
|
||||||
listState: LazyListState,
|
|
||||||
selectedUsers: ImmutableList<MatrixUser>,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onUserRemoved: (MatrixUser) -> Unit = {},
|
|
||||||
) {
|
|
||||||
LazyRow(
|
|
||||||
state = listState,
|
|
||||||
modifier = modifier,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
|
||||||
) {
|
|
||||||
items(selectedUsers.toList()) { matrixUser ->
|
|
||||||
SelectedUser(
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
onUserRemoved = onUserRemoved,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SelectedUser(
|
|
||||||
matrixUser: MatrixUser,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onUserRemoved: (MatrixUser) -> Unit,
|
|
||||||
) {
|
|
||||||
Box(modifier = modifier.width(56.dp)) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Avatar(matrixUser.avatarData.copy(size = AvatarSize.Custom(56.dp)))
|
|
||||||
Text(
|
|
||||||
text = matrixUser.getBestName(),
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
maxLines = 1,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.primary)
|
|
||||||
.size(20.dp)
|
|
||||||
.align(Alignment.TopEnd),
|
|
||||||
onClick = { onUserRemoved(matrixUser) }
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Close,
|
|
||||||
contentDescription = stringResource(id = StringR.string.action_remove),
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
internal fun UserListViewLightPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) =
|
|
||||||
ElementPreviewLight { ContentToPreview(state) }
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
internal fun UserListViewDarkPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) =
|
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContentToPreview(state: UserListState) {
|
|
||||||
UserListView(state = state)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* 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.userlist.api.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchMultipleUsersResultItem(
|
||||||
|
matrixUser: MatrixUser,
|
||||||
|
isUserSelected: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onCheckedChange: (Boolean) -> Unit = {},
|
||||||
|
) {
|
||||||
|
CheckableMatrixUserRow(
|
||||||
|
checked = isUserSelected,
|
||||||
|
modifier = modifier,
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
avatarSize = AvatarSize.Custom(36.dp),
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SearchMultipleUsersResultItemLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SearchMultipleUsersResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
Column {
|
||||||
|
SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = true)
|
||||||
|
SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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.userlist.api.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchSingleUserResultItem(
|
||||||
|
matrixUser: MatrixUser,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
MatrixUserRow(
|
||||||
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
avatarSize = AvatarSize.Custom(36.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SearchSingleUserResultItemLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SearchSingleUserResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
SearchSingleUserResultItem(matrixUser = aMatrixUser())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* 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.userlist.api.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
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.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SearchUserBar(
|
||||||
|
query: String,
|
||||||
|
results: ImmutableList<MatrixUser>,
|
||||||
|
selectedUsers: ImmutableList<MatrixUser>,
|
||||||
|
active: Boolean,
|
||||||
|
isMultiSelectionEnabled: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
placeHolderTitle: String = stringResource(R.string.common_search_for_someone),
|
||||||
|
onActiveChanged: (Boolean) -> Unit = {},
|
||||||
|
onTextChanged: (String) -> Unit = {},
|
||||||
|
onUserSelected: (MatrixUser) -> Unit = {},
|
||||||
|
onUserDeselected: (MatrixUser) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
onTextChanged("")
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = onTextChanged,
|
||||||
|
onSearch = { focusManager.clearFocus() },
|
||||||
|
active = active,
|
||||||
|
onActiveChange = onActiveChanged,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = placeHolderTitle,
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = if (active) {
|
||||||
|
{ BackButton(onClick = { onActiveChanged(false) }) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
trailingIcon = when {
|
||||||
|
active && query.isNotEmpty() -> {
|
||||||
|
{
|
||||||
|
IconButton(onClick = { onTextChanged("") }) {
|
||||||
|
Icon(Icons.Default.Close, stringResource(R.string.action_clear))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!active -> {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = stringResource(R.string.action_search),
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
content = {
|
||||||
|
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
|
||||||
|
SelectedUsersList(
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
selectedUsers = selectedUsers,
|
||||||
|
autoScroll = true,
|
||||||
|
onUserRemoved = onUserDeselected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn {
|
||||||
|
if (isMultiSelectionEnabled) {
|
||||||
|
items(results) { matrixUser ->
|
||||||
|
SearchMultipleUsersResultItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if (checked) {
|
||||||
|
onUserSelected(matrixUser)
|
||||||
|
} else {
|
||||||
|
onUserDeselected(matrixUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(results) { matrixUser ->
|
||||||
|
SearchSingleUserResultItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
onClick = { onUserSelected(matrixUser) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* 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.userlist.api.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
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.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectedUser(
|
||||||
|
matrixUser: MatrixUser,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onUserRemoved: (MatrixUser) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier.width(56.dp)) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Avatar(matrixUser.avatarData.copy(size = AvatarSize.Custom(56.dp)))
|
||||||
|
Text(
|
||||||
|
text = matrixUser.getBestName(),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primary)
|
||||||
|
.size(20.dp)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
|
onClick = { onUserRemoved(matrixUser) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(id = R.string.action_remove),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SelectedUserLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SelectedUserDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
SelectedUser(aMatrixUser())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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.userlist.api.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.userlist.api.aListOfSelectedUsers
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectedUsersList(
|
||||||
|
selectedUsers: ImmutableList<MatrixUser>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
autoScroll: Boolean = false,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
|
onUserRemoved: (MatrixUser) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
if (autoScroll) {
|
||||||
|
var currentSize by rememberSaveable { mutableStateOf(selectedUsers.size) }
|
||||||
|
LaunchedEffect(selectedUsers.size) {
|
||||||
|
val isItemAdded = selectedUsers.size > currentSize
|
||||||
|
if (isItemAdded) {
|
||||||
|
lazyListState.animateScrollToItem(selectedUsers.lastIndex)
|
||||||
|
}
|
||||||
|
currentSize = selectedUsers.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
state = lazyListState,
|
||||||
|
modifier = modifier,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
) {
|
||||||
|
items(selectedUsers.toList()) { matrixUser ->
|
||||||
|
SelectedUser(
|
||||||
|
matrixUser = matrixUser,
|
||||||
|
onUserRemoved = onUserRemoved,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SelectedUsersListLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun SelectedUsersListDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview() {
|
||||||
|
SelectedUsersList(
|
||||||
|
selectedUsers = aListOfSelectedUsers(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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.userlist.api.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.userlist.api.UserListEvents
|
||||||
|
import io.element.android.features.userlist.api.UserListState
|
||||||
|
import io.element.android.features.userlist.api.UserListStateProvider
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserListView(
|
||||||
|
state: UserListState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onUserSelected: (MatrixUser) -> Unit = {},
|
||||||
|
onUserDeselected: (MatrixUser) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
SearchUserBar(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
query = state.searchQuery,
|
||||||
|
results = state.searchResults,
|
||||||
|
selectedUsers = state.selectedUsers,
|
||||||
|
active = state.isSearchActive,
|
||||||
|
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
||||||
|
onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
|
||||||
|
onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
|
||||||
|
onUserSelected = {
|
||||||
|
state.eventSink(UserListEvents.AddToSelection(it))
|
||||||
|
onUserSelected(it)
|
||||||
|
},
|
||||||
|
onUserDeselected = {
|
||||||
|
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
||||||
|
onUserDeselected(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) {
|
||||||
|
SelectedUsersList(
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
selectedUsers = state.selectedUsers,
|
||||||
|
autoScroll = true,
|
||||||
|
onUserRemoved = {
|
||||||
|
state.eventSink(UserListEvents.RemoveFromSelection(it))
|
||||||
|
onUserDeselected(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun UserListViewLightPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) =
|
||||||
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun UserListViewDarkPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: UserListState) {
|
||||||
|
UserListView(state = state)
|
||||||
|
}
|
||||||
|
|
@ -16,26 +16,25 @@
|
||||||
|
|
||||||
package io.element.android.features.userlist.impl
|
package io.element.android.features.userlist.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
import io.element.android.features.userlist.api.UserListEvents
|
||||||
|
import io.element.android.features.userlist.api.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
import io.element.android.features.userlist.api.UserListState
|
import io.element.android.features.userlist.api.UserListState
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -43,28 +42,27 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class DefaultUserListPresenter @AssistedInject constructor(
|
class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
@Assisted val args: UserListPresenterArgs,
|
@Assisted val args: UserListPresenterArgs,
|
||||||
@Assisted val matrixUserDataSource: MatrixUserDataSource,
|
@Assisted val userListDataSource: UserListDataSource,
|
||||||
|
@Assisted val userListDataStore: UserListDataStore,
|
||||||
) : UserListPresenter {
|
) : UserListPresenter {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
interface DefaultUserListFactory : UserListPresenter.Factory {
|
interface DefaultUserListFactory : UserListPresenter.Factory {
|
||||||
override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): DefaultUserListPresenter
|
override fun create(
|
||||||
|
args: UserListPresenterArgs,
|
||||||
|
userListDataSource: UserListDataSource,
|
||||||
|
userListDataStore: UserListDataStore,
|
||||||
|
): DefaultUserListPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): UserListState {
|
override fun present(): UserListState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
|
||||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val selectedUsers: MutableState<ImmutableList<MatrixUser>> = remember {
|
val selectedUsers = userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||||
mutableStateOf(persistentListOf())
|
|
||||||
}
|
|
||||||
val selectedUsersListState = rememberLazyListState()
|
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
val searchResults: MutableState<ImmutableList<MatrixUser>> = remember {
|
val searchResults: MutableState<ImmutableList<MatrixUser>> = remember {
|
||||||
mutableStateOf(persistentListOf())
|
mutableStateOf(persistentListOf())
|
||||||
|
|
@ -74,13 +72,8 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
when (event) {
|
when (event) {
|
||||||
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||||
is UserListEvents.UpdateSearchQuery -> searchQuery = event.query
|
is UserListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||||
is UserListEvents.AddToSelection -> {
|
is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser)
|
||||||
if (event.matrixUser !in selectedUsers.value) {
|
is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser)
|
||||||
selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList()
|
|
||||||
}
|
|
||||||
localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState)
|
|
||||||
}
|
|
||||||
is UserListEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,8 +93,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
return UserListState(
|
return UserListState(
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
searchResults = searchResults.value,
|
searchResults = searchResults.value,
|
||||||
selectedUsers = selectedUsers.value.reversed().toImmutableList(),
|
selectedUsers = selectedUsers.value.toImmutableList(),
|
||||||
selectedUsersListState = selectedUsersListState,
|
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
selectionMode = args.selectionMode,
|
selectionMode = args.selectionMode,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
|
|
@ -110,16 +102,12 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun performSearch(query: String): ImmutableList<MatrixUser> {
|
private suspend fun performSearch(query: String): ImmutableList<MatrixUser> {
|
||||||
val isMatrixId = MatrixPatterns.isUserId(query)
|
val isMatrixId = MatrixPatterns.isUserId(query)
|
||||||
val results = matrixUserDataSource.search(query).toMutableList()
|
val results = userListDataSource.search(query).toMutableList()
|
||||||
if (isMatrixId && results.none { it.id.value == query }) {
|
if (isMatrixId && results.none { it.id.value == query }) {
|
||||||
val getProfileResult: MatrixUser? = matrixUserDataSource.getProfile(UserId(query))
|
val getProfileResult: MatrixUser? = userListDataSource.getProfile(UserId(query))
|
||||||
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
val profile = getProfileResult ?: MatrixUser(UserId(query))
|
||||||
results.add(0, profile)
|
results.add(0, profile)
|
||||||
}
|
}
|
||||||
return results.toImmutableList()
|
return results.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.scrollToFirstSelectedUser(listState: LazyListState) = launch {
|
|
||||||
listState.scrollToItem(index = 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,11 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
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.userlist.api.SelectionMode
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListEvents
|
import io.element.android.features.userlist.api.UserListEvents
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||||
import io.element.android.features.userlist.test.FakeMatrixUserDataSource
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
@ -37,13 +38,14 @@ import org.junit.Test
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class DefaultUserListPresenterTests {
|
class DefaultUserListPresenterTests {
|
||||||
|
|
||||||
private val userListDataSource = FakeMatrixUserDataSource()
|
private val userListDataSource = FakeUserListDataSource()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state for single selection`() = runTest {
|
fun `present - initial state for single selection`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter = DefaultUserListPresenter(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
userListDataSource
|
userListDataSource,
|
||||||
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -61,7 +63,8 @@ class DefaultUserListPresenterTests {
|
||||||
fun `present - initial state for multiple selection`() = runTest {
|
fun `present - initial state for multiple selection`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter = DefaultUserListPresenter(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
UserListPresenterArgs(selectionMode = SelectionMode.Multiple),
|
||||||
userListDataSource
|
userListDataSource,
|
||||||
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -79,7 +82,8 @@ class DefaultUserListPresenterTests {
|
||||||
fun `present - update search query`() = runTest {
|
fun `present - update search query`() = runTest {
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter = DefaultUserListPresenter(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
userListDataSource
|
userListDataSource,
|
||||||
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -111,7 +115,8 @@ class DefaultUserListPresenterTests {
|
||||||
|
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter = DefaultUserListPresenter(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
userListDataSource
|
userListDataSource,
|
||||||
|
UserListDataStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@
|
||||||
|
|
||||||
package io.element.android.features.userlist.test
|
package io.element.android.features.userlist.test
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
||||||
|
|
||||||
class FakeMatrixUserDataSource : MatrixUserDataSource {
|
class FakeUserListDataSource : UserListDataSource {
|
||||||
|
|
||||||
private var searchResult: List<MatrixUser> = emptyList()
|
private var searchResult: List<MatrixUser> = emptyList()
|
||||||
private var profile: MatrixUser? = null
|
private var profile: MatrixUser? = null
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.userlist.test
|
package io.element.android.features.userlist.test
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.MatrixUserDataSource
|
import io.element.android.features.userlist.api.UserListDataSource
|
||||||
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.userlist.api.UserListPresenter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
import io.element.android.features.userlist.api.UserListPresenterArgs
|
||||||
|
|
||||||
|
|
@ -24,5 +25,9 @@ class FakeUserListPresenterFactory(
|
||||||
private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter()
|
private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter()
|
||||||
) : UserListPresenter.Factory {
|
) : UserListPresenter.Factory {
|
||||||
|
|
||||||
override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter = fakeUserListPresenter
|
override fun create(
|
||||||
|
args: UserListPresenterArgs,
|
||||||
|
userListDataSource: UserListDataSource,
|
||||||
|
userListDataStore: UserListDataStore,
|
||||||
|
): UserListPresenter = fakeUserListPresenter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
android_gradle_plugin = "7.4.2"
|
android_gradle_plugin = "7.4.2"
|
||||||
kotlin = "1.8.10"
|
kotlin = "1.8.10"
|
||||||
ksp = "1.8.10-1.0.9"
|
ksp = "1.8.10-1.0.9"
|
||||||
molecule = "0.8.0"
|
molecule = "0.9.0"
|
||||||
|
|
||||||
# AndroidX
|
# AndroidX
|
||||||
material = "1.8.0"
|
material = "1.8.0"
|
||||||
|
|
@ -19,7 +19,7 @@ activity = "1.7.0"
|
||||||
startup = "1.1.1"
|
startup = "1.1.1"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2023.03.00"
|
compose_bom = "2023.04.00"
|
||||||
composecompiler = "1.4.2"
|
composecompiler = "1.4.2"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
|
|
@ -135,6 +135,8 @@ sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", vers
|
||||||
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3"
|
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3"
|
||||||
sqlite = "androidx.sqlite:sqlite:2.3.1"
|
sqlite = "androidx.sqlite:sqlite:2.3.1"
|
||||||
|
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
||||||
|
gujun_span = "me.gujun.android:span:1.7"
|
||||||
|
|
||||||
# Di
|
# Di
|
||||||
inject = "javax.inject:javax.inject:1"
|
inject = "javax.inject:javax.inject:1"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.ChecksSdkIntAtLeast
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
import io.element.android.libraries.androidutils.R
|
||||||
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
|
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -125,7 +126,7 @@ fun startNotificationSettingsIntent(context: Context, activityResultLauncher: Ac
|
||||||
|
|
||||||
fun openAppSettingsPage(
|
fun openAppSettingsPage(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = activity.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
activity.startActivity(
|
activity.startActivity(
|
||||||
|
|
@ -156,7 +157,7 @@ fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String
|
||||||
fun startAddGoogleAccountIntent(
|
fun startAddGoogleAccountIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
|
val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
|
||||||
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
|
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
|
||||||
|
|
@ -171,7 +172,7 @@ fun startAddGoogleAccountIntent(
|
||||||
fun startInstallFromSourceIntent(
|
fun startInstallFromSourceIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||||
.setData(Uri.parse(String.format("package:%s", context.packageName)))
|
.setData(Uri.parse(String.format("package:%s", context.packageName)))
|
||||||
|
|
@ -189,7 +190,7 @@ fun startSharePlainTextIntent(
|
||||||
text: String,
|
text: String,
|
||||||
subject: String? = null,
|
subject: String? = null,
|
||||||
extraTitle: String? = null,
|
extraTitle: String? = null,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
val share = Intent(Intent.ACTION_SEND)
|
val share = Intent(Intent.ACTION_SEND)
|
||||||
share.type = "text/plain"
|
share.type = "text/plain"
|
||||||
|
|
@ -217,7 +218,7 @@ fun startSharePlainTextIntent(
|
||||||
fun startImportTextFromFileIntent(
|
fun startImportTextFromFileIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||||
noActivityFoundMessage: String,
|
noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found),
|
||||||
) {
|
) {
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="error_no_compatible_app_found">"No se encontró ninguna aplicación compatible con esta acción."</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="error_no_compatible_app_found">"Non è stata trovata alcuna app compatibile per gestire questa azione."</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="error_no_compatible_app_found">"Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune."</string>
|
||||||
|
</resources>
|
||||||
4
libraries/androidutils/src/main/res/values/localazy.xml
Normal file
4
libraries/androidutils/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="error_no_compatible_app_found">"No compatible app was found to handle this action."</string>
|
||||||
|
</resources>
|
||||||
43
libraries/deeplink/build.gradle.kts
Normal file
43
libraries/deeplink/build.gradle.kts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
|
||||||
|
@Suppress("DSL_SCOPE_VIOLATION")
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.deeplink"
|
||||||
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.di)
|
||||||
|
implementation(libs.dagger)
|
||||||
|
implementation(libs.androidx.corektx)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.deeplink
|
||||||
|
|
||||||
|
internal const val SCHEME = "elementx"
|
||||||
|
internal const val HOST = "open"
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.deeplink
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DeepLinkCreator @Inject constructor() {
|
||||||
|
fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String {
|
||||||
|
return buildString {
|
||||||
|
append("$SCHEME://$HOST/")
|
||||||
|
append(sessionId.value)
|
||||||
|
if (roomId != null) {
|
||||||
|
append("/")
|
||||||
|
append(roomId.value)
|
||||||
|
if (threadId != null) {
|
||||||
|
append("/")
|
||||||
|
append(threadId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.deeplink
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
|
|
||||||
|
data class DeeplinkData(
|
||||||
|
val sessionId: SessionId,
|
||||||
|
val roomId: RoomId? = null,
|
||||||
|
val threadId: ThreadId? = null,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.deeplink
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import io.element.android.libraries.matrix.api.core.asRoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.asSessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.asThreadId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DeeplinkParser @Inject constructor() {
|
||||||
|
fun getFromIntent(intent: Intent): DeeplinkData? {
|
||||||
|
return intent
|
||||||
|
.takeIf { it.action == Intent.ACTION_VIEW }
|
||||||
|
?.data
|
||||||
|
?.toDeeplinkData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Uri.toDeeplinkData(): DeeplinkData? {
|
||||||
|
if (scheme != SCHEME) return null
|
||||||
|
if (host != HOST) return null
|
||||||
|
val pathBits = path.orEmpty().split("/").drop(1)
|
||||||
|
val sessionId = pathBits.elementAtOrNull(0)?.asSessionId() ?: return null
|
||||||
|
val roomId = pathBits.elementAtOrNull(1)?.asRoomId()
|
||||||
|
val threadId = pathBits.elementAtOrNull(2)?.asThreadId()
|
||||||
|
return DeeplinkData(
|
||||||
|
sessionId = sessionId,
|
||||||
|
roomId = roomId,
|
||||||
|
threadId = threadId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.deeplink
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DeepLinkCreatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun create() {
|
||||||
|
val sut = DeepLinkCreator()
|
||||||
|
assertThat(sut.create(A_SESSION_ID, null, null))
|
||||||
|
.isEqualTo("elementx://open/@alice:server.org")
|
||||||
|
assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null))
|
||||||
|
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
|
||||||
|
assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
|
||||||
|
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.deeplink
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||||
|
import io.element.android.tests.testutils.assertNullOrThrow
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class DeeplinkParserTest {
|
||||||
|
companion object {
|
||||||
|
const val A_URI =
|
||||||
|
"elementx://open/@alice:server.org"
|
||||||
|
const val A_URI_WITH_ROOM =
|
||||||
|
"elementx://open/@alice:server.org/!aRoomId:domain"
|
||||||
|
const val A_URI_WITH_ROOM_WITH_THREAD =
|
||||||
|
"elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sut = DeeplinkParser()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `nominal cases`() {
|
||||||
|
assertThat(sut.getFromIntent(createIntent(A_URI)))
|
||||||
|
.isEqualTo(DeeplinkData(A_SESSION_ID, null, null))
|
||||||
|
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM)))
|
||||||
|
.isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, null))
|
||||||
|
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_THREAD)))
|
||||||
|
.isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error cases`() {
|
||||||
|
val sut = DeeplinkParser()
|
||||||
|
// Bad scheme
|
||||||
|
assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull()
|
||||||
|
// Bad host
|
||||||
|
assertThat(sut.getFromIntent(createIntent("elementx://close/@alice:server.org"))).isNull()
|
||||||
|
// No session Id
|
||||||
|
assertThat(sut.getFromIntent(createIntent("elementx://open"))).isNull()
|
||||||
|
// Invalid sessionId
|
||||||
|
assertNullOrThrow {
|
||||||
|
sut.getFromIntent(createIntent("elementx://open/alice:server.org"))
|
||||||
|
}
|
||||||
|
// Empty sessionId
|
||||||
|
assertNullOrThrow {
|
||||||
|
sut.getFromIntent(createIntent("elementx://open//"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIntent(uri: String): Intent {
|
||||||
|
return Intent().apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
data = uri.toUri()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,20 @@
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.components.avatar
|
package io.element.android.libraries.designsystem.components.avatar
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@Parcelize
|
||||||
data class AvatarData(
|
data class AvatarData(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val url: String? = null,
|
val url: String? = null,
|
||||||
|
@IgnoredOnParcel
|
||||||
val size: AvatarSize = AvatarSize.MEDIUM
|
val size: AvatarSize = AvatarSize.MEDIUM
|
||||||
) {
|
) : Parcelable {
|
||||||
fun getInitial(): String {
|
fun getInitial(): String {
|
||||||
val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?'
|
val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?'
|
||||||
return firstChar.uppercase()
|
return firstChar.uppercase()
|
||||||
|
|
|
||||||
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