Merge pull request #1975 from element-hq/feature/bma/extractForward
Extract RoomList select to its own module
This commit is contained in:
commit
aa077baacd
45 changed files with 1011 additions and 468 deletions
|
|
@ -58,6 +58,7 @@ dependencies {
|
||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
|
implementation(projects.libraries.roomselect.api)
|
||||||
implementation(projects.libraries.voicerecorder.api)
|
implementation(projects.libraries.voicerecorder.api)
|
||||||
implementation(projects.libraries.mediaplayer.api)
|
implementation(projects.libraries.mediaplayer.api)
|
||||||
implementation(projects.libraries.uiUtils)
|
implementation(projects.libraries.uiUtils)
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.forward
|
package io.element.android.features.messages.impl.forward
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
|
||||||
|
|
||||||
sealed interface ForwardMessagesEvents {
|
sealed interface ForwardMessagesEvents {
|
||||||
data class SetSelectedRoom(val room: RoomSummaryDetails) : ForwardMessagesEvents
|
|
||||||
// TODO remove to restore multi-selection
|
|
||||||
data object RemoveSelectedRoom : ForwardMessagesEvents
|
|
||||||
data object ToggleSearchActive : ForwardMessagesEvents
|
|
||||||
data class UpdateQuery(val query: String) : ForwardMessagesEvents
|
|
||||||
data object ForwardEvent : ForwardMessagesEvents
|
|
||||||
data object ClearError : ForwardMessagesEvents
|
data object ClearError : ForwardMessagesEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,15 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.forward
|
package io.element.android.features.messages.impl.forward
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
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.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.node.ParentNode
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -29,14 +34,28 @@ import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class ForwardMessagesNode @AssistedInject constructor(
|
class ForwardMessagesNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: ForwardMessagesPresenter.Factory,
|
presenterFactory: ForwardMessagesPresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
private val roomSelectEntryPoint: RoomSelectEntryPoint,
|
||||||
|
) : ParentNode<ForwardMessagesNode.NavTarget>(
|
||||||
|
navModel = PermanentNavModel(
|
||||||
|
navTargets = setOf(NavTarget),
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object NavTarget : Parcelable
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onForwardedToSingleRoom(roomId: RoomId)
|
fun onForwardedToSingleRoom(roomId: RoomId)
|
||||||
|
|
@ -48,6 +67,39 @@ class ForwardMessagesNode @AssistedInject constructor(
|
||||||
private val presenter = presenterFactory.create(inputs.eventId.value)
|
private val presenter = presenterFactory.create(inputs.eventId.value)
|
||||||
private val callbacks = plugins.filterIsInstance<Callback>()
|
private val callbacks = plugins.filterIsInstance<Callback>()
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
val callback = object : RoomSelectEntryPoint.Callback {
|
||||||
|
override fun onRoomSelected(roomIds: List<RoomId>) {
|
||||||
|
presenter.onRoomSelected(roomIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel() {
|
||||||
|
navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomSelectEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.callback(callback)
|
||||||
|
.params(RoomSelectEntryPoint.Params(mode = RoomSelectMode.Forward))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
// Will render to room select screen
|
||||||
|
Children(
|
||||||
|
navModel = navModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
val state = presenter.present()
|
||||||
|
ForwardMessagesView(
|
||||||
|
state = state,
|
||||||
|
onForwardingSucceeded = ::onSucceeded,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onSucceeded(roomIds: ImmutableList<RoomId>) {
|
private fun onSucceeded(roomIds: ImmutableList<RoomId>) {
|
||||||
navigateUp()
|
navigateUp()
|
||||||
if (roomIds.size == 1) {
|
if (roomIds.size == 1) {
|
||||||
|
|
@ -55,15 +107,4 @@ class ForwardMessagesNode @AssistedInject constructor(
|
||||||
callbacks.forEach { it.onForwardedToSingleRoom(targetRoomId) }
|
callbacks.forEach { it.onForwardedToSingleRoom(targetRoomId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun View(modifier: Modifier) {
|
|
||||||
val state = presenter.present()
|
|
||||||
ForwardMessagesView(
|
|
||||||
state = state,
|
|
||||||
onDismiss = ::navigateUp,
|
|
||||||
onForwardingSucceeded = ::onSucceeded,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,28 +17,20 @@
|
||||||
package io.element.android.features.messages.impl.forward
|
package io.element.android.features.messages.impl.forward
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
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.setValue
|
|
||||||
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.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -47,7 +39,6 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
||||||
@Assisted eventId: String,
|
@Assisted eventId: String,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val matrixCoroutineScope: CoroutineScope,
|
private val matrixCoroutineScope: CoroutineScope,
|
||||||
private val client: MatrixClient,
|
|
||||||
) : Presenter<ForwardMessagesState> {
|
) : Presenter<ForwardMessagesState> {
|
||||||
|
|
||||||
private val eventId: EventId = EventId(eventId)
|
private val eventId: EventId = EventId(eventId)
|
||||||
|
|
@ -57,62 +48,25 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
||||||
fun create(eventId: String): ForwardMessagesPresenter
|
fun create(eventId: String): ForwardMessagesPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = mutableStateOf(Async.Uninitialized)
|
||||||
|
|
||||||
|
fun onRoomSelected(roomIds: List<RoomId>) {
|
||||||
|
matrixCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ForwardMessagesState {
|
override fun present(): ForwardMessagesState {
|
||||||
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummaryDetails>()) }
|
|
||||||
var query by remember { mutableStateOf<String>("") }
|
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
|
||||||
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
|
|
||||||
val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = remember { mutableStateOf(Async.Uninitialized) }
|
|
||||||
|
|
||||||
val summaries by client.roomListService.allRooms.summaries.collectAsState()
|
|
||||||
|
|
||||||
LaunchedEffect(query, summaries) {
|
|
||||||
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
|
||||||
.map { it.details }
|
|
||||||
.filter { it.name.contains(query, ignoreCase = true) }
|
|
||||||
.distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received
|
|
||||||
.toPersistentList()
|
|
||||||
results = if (filteredSummaries.isNotEmpty()) {
|
|
||||||
SearchBarResultState.Results(filteredSummaries)
|
|
||||||
} else {
|
|
||||||
SearchBarResultState.NoResults()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val forwardingSucceeded by remember {
|
val forwardingSucceeded by remember {
|
||||||
derivedStateOf { forwardingActionState.value.dataOrNull() }
|
derivedStateOf { forwardingActionState.value.dataOrNull() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvents(event: ForwardMessagesEvents) {
|
fun handleEvents(event: ForwardMessagesEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ForwardMessagesEvents.SetSelectedRoom -> {
|
|
||||||
selectedRooms = persistentListOf(event.room)
|
|
||||||
// Restore for multi-selection
|
|
||||||
// val index = selectedRooms.indexOfFirst { it.roomId == event.room.roomId }
|
|
||||||
// selectedRooms = if (index >= 0) {
|
|
||||||
// selectedRooms.removeAt(index)
|
|
||||||
// } else {
|
|
||||||
// selectedRooms.add(event.room)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
ForwardMessagesEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
|
||||||
is ForwardMessagesEvents.UpdateQuery -> query = event.query
|
|
||||||
ForwardMessagesEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
|
||||||
ForwardMessagesEvents.ForwardEvent -> {
|
|
||||||
isSearchActive = false
|
|
||||||
val roomIds = selectedRooms.map { it.roomId }.toPersistentList()
|
|
||||||
matrixCoroutineScope.forwardEvent(eventId, roomIds, forwardingActionState)
|
|
||||||
}
|
|
||||||
ForwardMessagesEvents.ClearError -> forwardingActionState.value = Async.Uninitialized
|
ForwardMessagesEvents.ClearError -> forwardingActionState.value = Async.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ForwardMessagesState(
|
return ForwardMessagesState(
|
||||||
resultState = results,
|
|
||||||
query = query,
|
|
||||||
isSearchActive = isSearchActive,
|
|
||||||
selectedRooms = selectedRooms,
|
|
||||||
isForwarding = forwardingActionState.value.isLoading(),
|
isForwarding = forwardingActionState.value.isLoading(),
|
||||||
error = (forwardingActionState.value as? Async.Failure)?.error,
|
error = (forwardingActionState.value as? Async.Failure)?.error,
|
||||||
forwardingSucceeded = forwardingSucceeded,
|
forwardingSucceeded = forwardingSucceeded,
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,11 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.forward
|
package io.element.android.features.messages.impl.forward
|
||||||
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ForwardMessagesState(
|
data class ForwardMessagesState(
|
||||||
val resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>>,
|
// TODO Migrate to an Async
|
||||||
val query: String,
|
|
||||||
val isSearchActive: Boolean,
|
|
||||||
val selectedRooms: ImmutableList<RoomSummaryDetails>,
|
|
||||||
val isForwarding: Boolean,
|
val isForwarding: Boolean,
|
||||||
val error: Throwable?,
|
val error: Throwable?,
|
||||||
val forwardingSucceeded: ImmutableList<RoomId>?,
|
val forwardingSucceeded: ImmutableList<RoomId>?,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl.forward
|
package io.element.android.features.messages.impl.forward
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||||
|
|
@ -29,38 +28,13 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider<ForwardMessag
|
||||||
override val values: Sequence<ForwardMessagesState>
|
override val values: Sequence<ForwardMessagesState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aForwardMessagesState(),
|
aForwardMessagesState(),
|
||||||
aForwardMessagesState(query = "Test", isSearchActive = true),
|
|
||||||
aForwardMessagesState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList())),
|
|
||||||
aForwardMessagesState(
|
aForwardMessagesState(
|
||||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
|
||||||
query = "Test",
|
|
||||||
isSearchActive = true,
|
|
||||||
),
|
|
||||||
aForwardMessagesState(
|
|
||||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
|
||||||
query = "Test",
|
|
||||||
isSearchActive = true,
|
|
||||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain")))
|
|
||||||
),
|
|
||||||
aForwardMessagesState(
|
|
||||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
|
||||||
query = "Test",
|
|
||||||
isSearchActive = true,
|
|
||||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))),
|
|
||||||
isForwarding = true,
|
isForwarding = true,
|
||||||
),
|
),
|
||||||
aForwardMessagesState(
|
aForwardMessagesState(
|
||||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
|
||||||
query = "Test",
|
|
||||||
isSearchActive = true,
|
|
||||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))),
|
|
||||||
forwardingSucceeded = persistentListOf(RoomId("!room2:domain")),
|
forwardingSucceeded = persistentListOf(RoomId("!room2:domain")),
|
||||||
),
|
),
|
||||||
aForwardMessagesState(
|
aForwardMessagesState(
|
||||||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
|
||||||
query = "Test",
|
|
||||||
isSearchActive = true,
|
|
||||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))),
|
|
||||||
error = Throwable("error"),
|
error = Throwable("error"),
|
||||||
),
|
),
|
||||||
// Add other states here
|
// Add other states here
|
||||||
|
|
@ -68,18 +42,10 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider<ForwardMessag
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aForwardMessagesState(
|
fun aForwardMessagesState(
|
||||||
resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>> = SearchBarResultState.NotSearching(),
|
|
||||||
query: String = "",
|
|
||||||
isSearchActive: Boolean = false,
|
|
||||||
selectedRooms: ImmutableList<RoomSummaryDetails> = persistentListOf(),
|
|
||||||
isForwarding: Boolean = false,
|
isForwarding: Boolean = false,
|
||||||
error: Throwable? = null,
|
error: Throwable? = null,
|
||||||
forwardingSucceeded: ImmutableList<RoomId>? = null,
|
forwardingSucceeded: ImmutableList<RoomId>? = null,
|
||||||
) = ForwardMessagesState(
|
) = ForwardMessagesState(
|
||||||
resultState = resultState,
|
|
||||||
query = query,
|
|
||||||
isSearchActive = isSearchActive,
|
|
||||||
selectedRooms = selectedRooms,
|
|
||||||
isForwarding = isForwarding,
|
isForwarding = isForwarding,
|
||||||
error = error,
|
error = error,
|
||||||
forwardingSucceeded = forwardingSucceeded,
|
forwardingSucceeded = forwardingSucceeded,
|
||||||
|
|
|
||||||
|
|
@ -16,63 +16,20 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.forward
|
package io.element.android.features.messages.impl.forward
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
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.dialogs.ErrorDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogDefaults
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogDefaults
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
|
||||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
|
||||||
import io.element.android.libraries.designsystem.theme.roomListRoomName
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ForwardMessagesView(
|
fun ForwardMessagesView(
|
||||||
state: ForwardMessagesState,
|
state: ForwardMessagesState,
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onForwardingSucceeded: (ImmutableList<RoomId>) -> Unit,
|
onForwardingSucceeded: (ImmutableList<RoomId>) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -81,193 +38,16 @@ fun ForwardMessagesView(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
if (state.isForwarding) {
|
||||||
fun onRoomRemoved(roomSummaryDetails: RoomSummaryDetails) {
|
ProgressDialog(modifier)
|
||||||
// TODO toggle selection when multi-selection is enabled
|
|
||||||
state.eventSink(ForwardMessagesEvents.RemoveSelectedRoom)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
if (state.error != null) {
|
||||||
fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<RoomSummaryDetails>) {
|
ForwardingErrorDialog(
|
||||||
if (isForwarding) return
|
modifier = modifier,
|
||||||
SelectedRooms(
|
onDismiss = { state.eventSink(ForwardMessagesEvents.ClearError) },
|
||||||
selectedRooms = selectedRooms,
|
|
||||||
onRoomRemoved = ::onRoomRemoved,
|
|
||||||
modifier = Modifier.padding(vertical = 16.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBackButton(state: ForwardMessagesState) {
|
|
||||||
if (state.isSearchActive) {
|
|
||||||
state.eventSink(ForwardMessagesEvents.ToggleSearchActive)
|
|
||||||
} else {
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BackHandler(onBack = { onBackButton(state) })
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
modifier = modifier,
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(CommonStrings.common_forward_message),
|
|
||||||
style = ElementTheme.typography.aliasScreenTitle
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
BackButton(onClick = { onBackButton(state) })
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
TextButton(
|
|
||||||
text = stringResource(CommonStrings.action_send),
|
|
||||||
enabled = state.selectedRooms.isNotEmpty(),
|
|
||||||
onClick = { state.eventSink(ForwardMessagesEvents.ForwardEvent) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.consumeWindowInsets(paddingValues)
|
|
||||||
) {
|
|
||||||
SearchBar(
|
|
||||||
placeHolderTitle = stringResource(CommonStrings.action_search),
|
|
||||||
query = state.query,
|
|
||||||
onQueryChange = { state.eventSink(ForwardMessagesEvents.UpdateQuery(it)) },
|
|
||||||
active = state.isSearchActive,
|
|
||||||
onActiveChange = { state.eventSink(ForwardMessagesEvents.ToggleSearchActive) },
|
|
||||||
resultState = state.resultState,
|
|
||||||
showBackButton = false,
|
|
||||||
) { summaries ->
|
|
||||||
LazyColumn {
|
|
||||||
item {
|
|
||||||
SelectedRoomsHelper(
|
|
||||||
isForwarding = state.isForwarding,
|
|
||||||
selectedRooms = state.selectedRooms
|
|
||||||
)
|
|
||||||
}
|
|
||||||
items(summaries, key = { it.roomId.value }) { roomSummary ->
|
|
||||||
Column {
|
|
||||||
RoomSummaryView(
|
|
||||||
roomSummary,
|
|
||||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
|
||||||
onSelection = { roomSummary ->
|
|
||||||
state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.isSearchActive) {
|
|
||||||
// TODO restore for multi-selection
|
|
||||||
// SelectedRoomsHelper(
|
|
||||||
// isForwarding = state.isForwarding,
|
|
||||||
// selectedRooms = state.selectedRooms
|
|
||||||
// )
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
|
|
||||||
if (state.resultState is SearchBarResultState.Results) {
|
|
||||||
LazyColumn {
|
|
||||||
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
|
||||||
Column {
|
|
||||||
RoomSummaryView(
|
|
||||||
roomSummary,
|
|
||||||
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
|
||||||
onSelection = { roomSummary ->
|
|
||||||
state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.isForwarding) {
|
|
||||||
ProgressDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.error != null) {
|
|
||||||
ForwardingErrorDialog(onDismiss = { state.eventSink(ForwardMessagesEvents.ClearError) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SelectedRooms(
|
|
||||||
selectedRooms: ImmutableList<RoomSummaryDetails>,
|
|
||||||
onRoomRemoved: (RoomSummaryDetails) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
LazyRow(
|
|
||||||
modifier,
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
|
||||||
) {
|
|
||||||
items(selectedRooms, key = { it.roomId.value }) { roomSummary ->
|
|
||||||
SelectedRoom(roomSummary = roomSummary, onRoomRemoved = onRoomRemoved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RoomSummaryView(
|
|
||||||
summary: RoomSummaryDetails,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onSelection: (RoomSummaryDetails) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.clickable { onSelection(summary) }
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, end = 4.dp)
|
|
||||||
.heightIn(56.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
val roomAlias = summary.canonicalAlias ?: summary.roomId.value
|
|
||||||
Avatar(
|
|
||||||
avatarData = AvatarData(
|
|
||||||
id = roomAlias,
|
|
||||||
name = summary.name,
|
|
||||||
url = summary.avatarURLString,
|
|
||||||
size = AvatarSize.ForwardRoomListItem,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
// Name
|
|
||||||
Text(
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
text = summary.name,
|
|
||||||
color = MaterialTheme.roomListRoomName(),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
// Id
|
|
||||||
Text(
|
|
||||||
text = roomAlias,
|
|
||||||
color = MaterialTheme.roomListRoomMessage(),
|
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RadioButton(selected = isSelected, onClick = { onSelection(summary) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -284,7 +64,6 @@ private fun ForwardingErrorDialog(onDismiss: () -> Unit, modifier: Modifier = Mo
|
||||||
internal fun ForwardMessagesViewPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) = ElementPreview {
|
internal fun ForwardMessagesViewPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) = ElementPreview {
|
||||||
ForwardMessagesView(
|
ForwardMessagesView(
|
||||||
state = state,
|
state = state,
|
||||||
onDismiss = {},
|
|
||||||
onForwardingSucceeded = {}
|
onForwardingSucceeded = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,11 @@ import app.cash.molecule.RecompositionMode
|
||||||
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.libraries.designsystem.theme.components.SearchBarResultState
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -40,7 +35,6 @@ class ForwardMessagesPresenterTests {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = aPresenter()
|
val presenter = aPresenter()
|
||||||
|
|
@ -48,75 +42,23 @@ class ForwardMessagesPresenterTests {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.selectedRooms).isEmpty()
|
|
||||||
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
|
||||||
assertThat(initialState.isSearchActive).isFalse()
|
|
||||||
assertThat(initialState.isForwarding).isFalse()
|
assertThat(initialState.isForwarding).isFalse()
|
||||||
assertThat(initialState.error).isNull()
|
assertThat(initialState.error).isNull()
|
||||||
assertThat(initialState.forwardingSucceeded).isNull()
|
assertThat(initialState.forwardingSucceeded).isNull()
|
||||||
|
|
||||||
// Search is run automatically
|
|
||||||
val searchState = awaitItem()
|
|
||||||
assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - toggle search active`() = runTest {
|
fun `present - forward successful`() = runTest {
|
||||||
val presenter = aPresenter()
|
val presenter = aPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
initialState.eventSink(ForwardMessagesEvents.ToggleSearchActive)
|
|
||||||
assertThat(awaitItem().isSearchActive).isTrue()
|
|
||||||
|
|
||||||
initialState.eventSink(ForwardMessagesEvents.ToggleSearchActive)
|
|
||||||
assertThat(awaitItem().isSearchActive).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - update query`() = runTest {
|
|
||||||
val roomListService = FakeRoomListService().apply {
|
|
||||||
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
|
|
||||||
}
|
|
||||||
val client = FakeMatrixClient(roomListService = roomListService)
|
|
||||||
val presenter = aPresenter(client = client)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetail())))
|
|
||||||
|
|
||||||
initialState.eventSink(ForwardMessagesEvents.UpdateQuery("string not contained"))
|
|
||||||
assertThat(awaitItem().query).isEqualTo("string not contained")
|
|
||||||
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - select a room and forward successful`() = runTest {
|
|
||||||
val presenter = aPresenter()
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val summary = aRoomSummaryDetail()
|
val summary = aRoomSummaryDetail()
|
||||||
|
presenter.onRoomSelected(listOf(summary.roomId))
|
||||||
initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary))
|
|
||||||
awaitItem()
|
|
||||||
|
|
||||||
// Test successful forwarding
|
|
||||||
initialState.eventSink(ForwardMessagesEvents.ForwardEvent)
|
|
||||||
|
|
||||||
val forwardingState = awaitItem()
|
val forwardingState = awaitItem()
|
||||||
assertThat(forwardingState.isSearchActive).isFalse()
|
|
||||||
assertThat(forwardingState.isForwarding).isTrue()
|
assertThat(forwardingState.isForwarding).isTrue()
|
||||||
|
|
||||||
val successfulForwardState = awaitItem()
|
val successfulForwardState = awaitItem()
|
||||||
assertThat(successfulForwardState.isForwarding).isFalse()
|
assertThat(successfulForwardState.isForwarding).isFalse()
|
||||||
assertThat(successfulForwardState.forwardingSucceeded).isNotNull()
|
assertThat(successfulForwardState.forwardingSucceeded).isNotNull()
|
||||||
|
|
@ -130,43 +72,17 @@ class ForwardMessagesPresenterTests {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
|
||||||
skipItems(1)
|
|
||||||
val summary = aRoomSummaryDetail()
|
|
||||||
|
|
||||||
initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary))
|
|
||||||
awaitItem()
|
|
||||||
|
|
||||||
// Test failed forwarding
|
// Test failed forwarding
|
||||||
room.givenForwardEventResult(Result.failure(Throwable("error")))
|
room.givenForwardEventResult(Result.failure(Throwable("error")))
|
||||||
initialState.eventSink(ForwardMessagesEvents.ForwardEvent)
|
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
val failedForwardState = awaitItem()
|
|
||||||
assertThat(failedForwardState.isForwarding).isFalse()
|
|
||||||
assertThat(failedForwardState.error).isNotNull()
|
|
||||||
|
|
||||||
// Then clear error
|
|
||||||
initialState.eventSink(ForwardMessagesEvents.ClearError)
|
|
||||||
assertThat(awaitItem().error).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - select and remove a room`() = runTest {
|
|
||||||
val presenter = aPresenter()
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val summary = aRoomSummaryDetail()
|
val summary = aRoomSummaryDetail()
|
||||||
|
presenter.onRoomSelected(listOf(summary.roomId))
|
||||||
initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary))
|
skipItems(1)
|
||||||
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
|
val failedForwardState = awaitItem()
|
||||||
|
assertThat(failedForwardState.error).isNotNull()
|
||||||
initialState.eventSink(ForwardMessagesEvents.RemoveSelectedRoom)
|
// Then clear error
|
||||||
assertThat(awaitItem().selectedRooms).isEmpty()
|
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
|
||||||
|
assertThat(awaitItem().error).isNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,6 +90,9 @@ class ForwardMessagesPresenterTests {
|
||||||
eventId: EventId = AN_EVENT_ID,
|
eventId: EventId = AN_EVENT_ID,
|
||||||
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
|
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
|
||||||
coroutineScope: CoroutineScope = this,
|
coroutineScope: CoroutineScope = this,
|
||||||
client: FakeMatrixClient = FakeMatrixClient(),
|
) = ForwardMessagesPresenter(
|
||||||
) = ForwardMessagesPresenter(eventId.value, fakeMatrixRoom, coroutineScope, client)
|
eventId = eventId.value,
|
||||||
|
room = fakeMatrixRoom,
|
||||||
|
matrixCoroutineScope = coroutineScope,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
libraries/roomselect/api/build.gradle.kts
Normal file
27
libraries/roomselect/api/build.gradle.kts
Normal file
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.roomselect.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.api
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
interface RoomSelectEntryPoint : FeatureEntryPoint {
|
||||||
|
data class Params(
|
||||||
|
val mode: RoomSelectMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||||
|
interface NodeBuilder {
|
||||||
|
fun params(params: Params): NodeBuilder
|
||||||
|
fun callback(callback: Callback): NodeBuilder
|
||||||
|
fun build(): Node
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onRoomSelected(roomIds: List<RoomId>)
|
||||||
|
fun onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.api
|
||||||
|
|
||||||
|
enum class RoomSelectMode {
|
||||||
|
Forward,
|
||||||
|
}
|
||||||
52
libraries/roomselect/impl/build.gradle.kts
Normal file
52
libraries/roomselect/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.roomselect.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.anvilannotations)
|
||||||
|
anvil(projects.anvilcodegen)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
|
api(projects.libraries.roomselect.api)
|
||||||
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.impl
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultRoomSelectEntryPoint @Inject constructor() : RoomSelectEntryPoint {
|
||||||
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomSelectEntryPoint.NodeBuilder {
|
||||||
|
val plugins = ArrayList<Plugin>()
|
||||||
|
|
||||||
|
return object : RoomSelectEntryPoint.NodeBuilder {
|
||||||
|
override fun params(params: RoomSelectEntryPoint.Params): RoomSelectEntryPoint.NodeBuilder {
|
||||||
|
plugins += RoomSelectNode.Inputs(mode = params.mode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun callback(callback: RoomSelectEntryPoint.Callback): RoomSelectEntryPoint.NodeBuilder {
|
||||||
|
plugins += callback
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(): Node {
|
||||||
|
return parentNode.createNode<RoomSelectNode>(buildContext, plugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
|
|
||||||
|
sealed interface RoomSelectEvents {
|
||||||
|
data class SetSelectedRoom(val room: RoomSummaryDetails) : RoomSelectEvents
|
||||||
|
|
||||||
|
// TODO remove to restore multi-selection
|
||||||
|
data object RemoveSelectedRoom : RoomSelectEvents
|
||||||
|
data object ToggleSearchActive : RoomSelectEvents
|
||||||
|
data class UpdateQuery(val query: String) : RoomSelectEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class RoomSelectNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
presenterFactory: RoomSelectPresenter.Factory,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
data class Inputs(
|
||||||
|
val mode: RoomSelectMode,
|
||||||
|
) : NodeInputs
|
||||||
|
|
||||||
|
private val inputs: Inputs = inputs()
|
||||||
|
private val presenter = presenterFactory.create(inputs.mode)
|
||||||
|
|
||||||
|
private val callbacks = plugins.filterIsInstance<RoomSelectEntryPoint.Callback>()
|
||||||
|
|
||||||
|
private fun onDismiss() {
|
||||||
|
callbacks.forEach { it.onCancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSubmit(roomIds: List<RoomId>) {
|
||||||
|
callbacks.forEach { it.onRoomSelected(roomIds) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
RoomSelectView(
|
||||||
|
state = state,
|
||||||
|
onDismiss = ::onDismiss,
|
||||||
|
onSubmit = ::onSubmit,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
|
class RoomSelectPresenter @AssistedInject constructor(
|
||||||
|
@Assisted private val mode: RoomSelectMode,
|
||||||
|
private val client: MatrixClient,
|
||||||
|
) : Presenter<RoomSelectState> {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(mode: RoomSelectMode): RoomSelectPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): RoomSelectState {
|
||||||
|
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummaryDetails>()) }
|
||||||
|
var query by remember { mutableStateOf("") }
|
||||||
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
|
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
|
||||||
|
|
||||||
|
val summaries by client.roomListService.allRooms.summaries.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(query, summaries) {
|
||||||
|
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
||||||
|
.map { it.details }
|
||||||
|
.filter { it.name.contains(query, ignoreCase = true) }
|
||||||
|
.distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received
|
||||||
|
.toPersistentList()
|
||||||
|
results = if (filteredSummaries.isNotEmpty()) {
|
||||||
|
SearchBarResultState.Results(filteredSummaries)
|
||||||
|
} else {
|
||||||
|
SearchBarResultState.NoResults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: RoomSelectEvents) {
|
||||||
|
when (event) {
|
||||||
|
is RoomSelectEvents.SetSelectedRoom -> {
|
||||||
|
selectedRooms = persistentListOf(event.room)
|
||||||
|
// Restore for multi-selection
|
||||||
|
// val index = selectedRooms.indexOfFirst { it.roomId == event.room.roomId }
|
||||||
|
// selectedRooms = if (index >= 0) {
|
||||||
|
// selectedRooms.removeAt(index)
|
||||||
|
// } else {
|
||||||
|
// selectedRooms.add(event.room)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf()
|
||||||
|
is RoomSelectEvents.UpdateQuery -> query = event.query
|
||||||
|
RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoomSelectState(
|
||||||
|
mode = mode,
|
||||||
|
resultState = results,
|
||||||
|
query = query,
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
selectedRooms = selectedRooms,
|
||||||
|
eventSink = { handleEvents(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
data class RoomSelectState(
|
||||||
|
val mode: RoomSelectMode,
|
||||||
|
val resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>>,
|
||||||
|
val query: String,
|
||||||
|
val isSearchActive: Boolean,
|
||||||
|
val selectedRooms: ImmutableList<RoomSummaryDetails>,
|
||||||
|
val eventSink: (RoomSelectEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.impl
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
|
||||||
|
override val values: Sequence<RoomSelectState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aRoomSelectState(),
|
||||||
|
aRoomSelectState(query = "Test", isSearchActive = true),
|
||||||
|
aRoomSelectState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList())),
|
||||||
|
aRoomSelectState(
|
||||||
|
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||||
|
query = "Test",
|
||||||
|
isSearchActive = true,
|
||||||
|
),
|
||||||
|
aRoomSelectState(
|
||||||
|
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||||
|
query = "Test",
|
||||||
|
isSearchActive = true,
|
||||||
|
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain")))
|
||||||
|
),
|
||||||
|
// Add other states here
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aRoomSelectState(
|
||||||
|
resultState: SearchBarResultState<ImmutableList<RoomSummaryDetails>> = SearchBarResultState.NotSearching(),
|
||||||
|
query: String = "",
|
||||||
|
isSearchActive: Boolean = false,
|
||||||
|
selectedRooms: ImmutableList<RoomSummaryDetails> = persistentListOf(),
|
||||||
|
) = RoomSelectState(
|
||||||
|
mode = RoomSelectMode.Forward,
|
||||||
|
resultState = resultState,
|
||||||
|
query = query,
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
selectedRooms = selectedRooms,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun aForwardMessagesRoomList() = persistentListOf(
|
||||||
|
aRoomDetailsState(),
|
||||||
|
aRoomDetailsState(roomId = RoomId("!room2:domain"), canonicalAlias = "#element-x-room:matrix.org"),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aRoomDetailsState(
|
||||||
|
roomId: RoomId = RoomId("!room:domain"),
|
||||||
|
name: String = "roomName",
|
||||||
|
canonicalAlias: String? = null,
|
||||||
|
isDirect: Boolean = true,
|
||||||
|
avatarURLString: String? = null,
|
||||||
|
lastMessage: RoomMessage? = null,
|
||||||
|
lastMessageTimestamp: Long? = null,
|
||||||
|
unreadNotificationCount: Int = 0,
|
||||||
|
inviter: RoomMember? = null,
|
||||||
|
) = RoomSummaryDetails(
|
||||||
|
roomId = roomId,
|
||||||
|
name = name,
|
||||||
|
canonicalAlias = canonicalAlias,
|
||||||
|
isDirect = isDirect,
|
||||||
|
avatarURLString = avatarURLString,
|
||||||
|
lastMessage = lastMessage,
|
||||||
|
lastMessageTimestamp = lastMessageTimestamp,
|
||||||
|
unreadNotificationCount = unreadNotificationCount,
|
||||||
|
inviter = inviter,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.impl
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
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.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
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.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
||||||
|
import io.element.android.libraries.designsystem.theme.roomListRoomName
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
|
import io.element.android.libraries.matrix.ui.components.SelectedRoom
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RoomSelectView(
|
||||||
|
state: RoomSelectState,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSubmit: (List<RoomId>) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun onRoomRemoved(roomSummaryDetails: RoomSummaryDetails) {
|
||||||
|
// TODO toggle selection when multi-selection is enabled
|
||||||
|
state.eventSink(RoomSelectEvents.RemoveSelectedRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<RoomSummaryDetails>) {
|
||||||
|
if (isForwarding) return
|
||||||
|
SelectedRooms(
|
||||||
|
selectedRooms = selectedRooms,
|
||||||
|
onRoomRemoved = ::onRoomRemoved,
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackButton(state: RoomSelectState) {
|
||||||
|
if (state.isSearchActive) {
|
||||||
|
state.eventSink(RoomSelectEvents.ToggleSearchActive)
|
||||||
|
} else {
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(onBack = { onBackButton(state) })
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = when (state.mode) {
|
||||||
|
RoomSelectMode.Forward -> stringResource(CommonStrings.common_forward_message)
|
||||||
|
},
|
||||||
|
style = ElementTheme.typography.aliasScreenTitle
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = { onBackButton(state) })
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(CommonStrings.action_send),
|
||||||
|
enabled = state.selectedRooms.isNotEmpty(),
|
||||||
|
onClick = { onSubmit(state.selectedRooms.map { it.roomId }) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.consumeWindowInsets(paddingValues)
|
||||||
|
) {
|
||||||
|
SearchBar(
|
||||||
|
placeHolderTitle = stringResource(CommonStrings.action_search),
|
||||||
|
query = state.query,
|
||||||
|
onQueryChange = { state.eventSink(RoomSelectEvents.UpdateQuery(it)) },
|
||||||
|
active = state.isSearchActive,
|
||||||
|
onActiveChange = { state.eventSink(RoomSelectEvents.ToggleSearchActive) },
|
||||||
|
resultState = state.resultState,
|
||||||
|
showBackButton = false,
|
||||||
|
) { summaries ->
|
||||||
|
LazyColumn {
|
||||||
|
item {
|
||||||
|
SelectedRoomsHelper(
|
||||||
|
isForwarding = false, // TODO state.isForwarding,
|
||||||
|
selectedRooms = state.selectedRooms
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(summaries, key = { it.roomId.value }) { roomSummary ->
|
||||||
|
Column {
|
||||||
|
RoomSummaryView(
|
||||||
|
roomSummary,
|
||||||
|
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
||||||
|
onSelection = { roomSummary ->
|
||||||
|
state.eventSink(RoomSelectEvents.SetSelectedRoom(roomSummary))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.isSearchActive) {
|
||||||
|
// TODO restore for multi-selection
|
||||||
|
// SelectedRoomsHelper(
|
||||||
|
// isForwarding = state.isForwarding,
|
||||||
|
// selectedRooms = state.selectedRooms
|
||||||
|
// )
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
if (state.resultState is SearchBarResultState.Results) {
|
||||||
|
LazyColumn {
|
||||||
|
items(state.resultState.results, key = { it.roomId.value }) { roomSummary ->
|
||||||
|
Column {
|
||||||
|
RoomSummaryView(
|
||||||
|
roomSummary,
|
||||||
|
isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId },
|
||||||
|
onSelection = { roomSummary ->
|
||||||
|
state.eventSink(RoomSelectEvents.SetSelectedRoom(roomSummary))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectedRooms(
|
||||||
|
selectedRooms: ImmutableList<RoomSummaryDetails>,
|
||||||
|
onRoomRemoved: (RoomSummaryDetails) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
modifier,
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
|
) {
|
||||||
|
items(selectedRooms, key = { it.roomId.value }) { roomSummary ->
|
||||||
|
SelectedRoom(roomSummary = roomSummary, onRoomRemoved = onRoomRemoved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomSummaryView(
|
||||||
|
summary: RoomSummaryDetails,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onSelection: (RoomSummaryDetails) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.clickable { onSelection(summary) }
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 4.dp)
|
||||||
|
.heightIn(56.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val roomAlias = summary.canonicalAlias ?: summary.roomId.value
|
||||||
|
Avatar(
|
||||||
|
avatarData = AvatarData(
|
||||||
|
id = roomAlias,
|
||||||
|
name = summary.name,
|
||||||
|
url = summary.avatarURLString,
|
||||||
|
size = AvatarSize.ForwardRoomListItem,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
// Name
|
||||||
|
Text(
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
text = summary.name,
|
||||||
|
color = MaterialTheme.roomListRoomName(),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
// Id
|
||||||
|
Text(
|
||||||
|
text = roomAlias,
|
||||||
|
color = MaterialTheme.roomListRoomMessage(),
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RadioButton(selected = isSelected, onClick = { onSelection(summary) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun RoomSelectViewPreview(@PreviewParameter(RoomSelectStateProvider::class) state: RoomSelectState) = ElementPreview {
|
||||||
|
RoomSelectView(
|
||||||
|
state = state,
|
||||||
|
onDismiss = {},
|
||||||
|
onSubmit = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* 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.roomselect.impl
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionMode
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail
|
||||||
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RoomSelectPresenterTests {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = aPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.selectedRooms).isEmpty()
|
||||||
|
assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||||
|
assertThat(initialState.isSearchActive).isFalse()
|
||||||
|
// Search is run automatically
|
||||||
|
val searchState = awaitItem()
|
||||||
|
assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - toggle search active`() = runTest {
|
||||||
|
val presenter = aPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
|
||||||
|
assertThat(awaitItem().isSearchActive).isTrue()
|
||||||
|
|
||||||
|
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
|
||||||
|
assertThat(awaitItem().isSearchActive).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - update query`() = runTest {
|
||||||
|
val roomListService = FakeRoomListService().apply {
|
||||||
|
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
|
||||||
|
}
|
||||||
|
val client = FakeMatrixClient(roomListService = roomListService)
|
||||||
|
val presenter = aPresenter(client = client)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetail())))
|
||||||
|
|
||||||
|
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
|
||||||
|
assertThat(awaitItem().query).isEqualTo("string not contained")
|
||||||
|
assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - select and remove a room`() = runTest {
|
||||||
|
val presenter = aPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
skipItems(1)
|
||||||
|
val summary = aRoomSummaryDetail()
|
||||||
|
|
||||||
|
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary))
|
||||||
|
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
|
||||||
|
|
||||||
|
initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom)
|
||||||
|
assertThat(awaitItem().selectedRooms).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aPresenter(
|
||||||
|
mode: RoomSelectMode = RoomSelectMode.Forward,
|
||||||
|
client: FakeMatrixClient = FakeMatrixClient(),
|
||||||
|
) = RoomSelectPresenter(
|
||||||
|
mode = mode,
|
||||||
|
client = client,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -102,6 +102,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
||||||
implementation(project(":libraries:mediaupload:impl"))
|
implementation(project(":libraries:mediaupload:impl"))
|
||||||
implementation(project(":libraries:usersearch:impl"))
|
implementation(project(":libraries:usersearch:impl"))
|
||||||
implementation(project(":libraries:textcomposer:impl"))
|
implementation(project(":libraries:textcomposer:impl"))
|
||||||
|
implementation(project(":libraries:roomselect:impl"))
|
||||||
implementation(project(":libraries:cryptography:impl"))
|
implementation(project(":libraries:cryptography:impl"))
|
||||||
implementation(project(":libraries:voicerecorder:impl"))
|
implementation(project(":libraries:voicerecorder:impl"))
|
||||||
implementation(project(":libraries:mediaplayer:impl"))
|
implementation(project(":libraries:mediaplayer:impl"))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:05fae38cdda7b3e00a355ecade04c54b5fa2921a529aa51af7cf90122caa9769
|
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||||
size 14972
|
size 4457
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:83af8ee61dc0ddf6fdbbe58d49266d78d3a0809c57b91ea5995bd1c47b1c00f9
|
oid sha256:cf70dc030f96b929ba3817694a624de715b82719117fc75720c4e3ff8a8188c8
|
||||||
size 12648
|
size 9962
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a4f6f8527d28a0c1c8cf0520b8927ae0e6f8adcaf0d56d0a117d48700dee89b7
|
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||||
size 28388
|
size 4457
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6349c7d27b2c227801212825e8f162da43f9b466ffb746b0f24b520a1fcdd146
|
oid sha256:a8e4490d507c06fae92e6f05aed7c31ae5a20e2a52bc23efc7db7d6e1290dc36
|
||||||
size 26298
|
size 10807
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:e0cd4916c256811a70c4ee39409f48328920a663524d65922adeeb518dba3b59
|
|
||||||
size 28225
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
|
||||||
size 4457
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:9e9ea671e95d6f6526c67e9a6048a1e9e91de1f42cd91a1b4a76138c8b08fe65
|
|
||||||
size 24884
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:764689e0dc5b509a5c36493ab7454ff5dc0c269fab6c68c57cbd89198b254506
|
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
||||||
size 13853
|
size 4464
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:323b97bd6852a87e23ebb29f06a8406931de786d0bc702cd464718ec8ba7f8e4
|
oid sha256:a8b6b2f2e2c03e7927ccb93d4fa8e914f7a3a19f4526cda8237e324dda3d9d3f
|
||||||
size 11799
|
size 7548
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e3d8829874805bdcfcb0b0978d9decbb463ec6d31f0d5cf5406dde0f1f786df9
|
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
||||||
size 27104
|
size 4464
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:29fb9eae961e82ca62e29ac5894dcdaac5290f69195cbb899e4862f522e1592c
|
oid sha256:a90e9ecf5cc6c042d9a72401b378ae0de46244803a9729fe339df7b7b676e603
|
||||||
size 25484
|
size 8225
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:e9fdef27a785ede0a878d6f59ff27843cb836361b3cbdf045f107d7980ea38bc
|
|
||||||
size 25759
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
|
||||||
size 4464
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:c98f350f39909171eb7b852efd43f434e7aaee31a8262ec079ba12879906077d
|
|
||||||
size 21721
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:05fae38cdda7b3e00a355ecade04c54b5fa2921a529aa51af7cf90122caa9769
|
||||||
|
size 14972
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:83af8ee61dc0ddf6fdbbe58d49266d78d3a0809c57b91ea5995bd1c47b1c00f9
|
||||||
|
size 12648
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a4f6f8527d28a0c1c8cf0520b8927ae0e6f8adcaf0d56d0a117d48700dee89b7
|
||||||
|
size 28388
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6349c7d27b2c227801212825e8f162da43f9b466ffb746b0f24b520a1fcdd146
|
||||||
|
size 26298
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:764689e0dc5b509a5c36493ab7454ff5dc0c269fab6c68c57cbd89198b254506
|
||||||
|
size 13853
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:323b97bd6852a87e23ebb29f06a8406931de786d0bc702cd464718ec8ba7f8e4
|
||||||
|
size 11799
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e3d8829874805bdcfcb0b0978d9decbb463ec6d31f0d5cf5406dde0f1f786df9
|
||||||
|
size 27104
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:29fb9eae961e82ca62e29ac5894dcdaac5290f69195cbb899e4862f522e1592c
|
||||||
|
size 25484
|
||||||
Loading…
Add table
Add a link
Reference in a new issue