First implementation of using Node/Presenter/UI on RoomList (no DI)

This commit is contained in:
ganfra 2023-01-03 19:51:04 +01:00
parent e176a41ecf
commit 1509d82f3f
13 changed files with 326 additions and 230 deletions

View file

@ -41,7 +41,7 @@ class MainActivity : NodeComponentActivity(), DaggerComponentOwner {
NodeHost(integrationPoint = appyxIntegrationPoint) {
RootFlowNode(
buildContext = it,
daggerComponentOwner = this,
appComponentOwner = this,
matrix = appBindings.matrix(),
sessionComponentsOwner = appBindings.sessionComponentsOwner()
)

View file

@ -4,6 +4,7 @@ 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.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
@ -12,14 +13,18 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.x.core.di.viewModelSupportNode
import io.element.android.x.features.messages.MessagesScreen
import io.element.android.x.features.roomlist.RoomListScreen
import io.element.android.x.features.roomlist.RoomListNode
import io.element.android.x.features.roomlist.RoomListPresenter
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.core.RoomId
import io.element.android.x.matrix.core.SessionId
import kotlinx.parcelize.Parcelize
import timber.log.Timber
class LoggedInFlowNode(
buildContext: BuildContext,
val sessionId: SessionId,
private val matrixClient: MatrixClient,
private val backstack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.RoomList,
savedStateMap = buildContext.savedStateMap,
@ -29,6 +34,13 @@ class LoggedInFlowNode(
buildContext = buildContext
) {
init {
lifecycle.subscribe(
onCreate = { Timber.v("OnCreate") },
onDestroy = { Timber.v("OnDestroy") }
)
}
sealed interface NavTarget : Parcelable {
@Parcelize
object RoomList : NavTarget
@ -39,11 +51,12 @@ class LoggedInFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.RoomList -> viewModelSupportNode(buildContext) {
RoomListScreen(
onRoomClicked = { backstack.push(NavTarget.Messages(it)) }
)
}
NavTarget.RoomList -> RoomListNode(
buildContext = buildContext,
presenter = RoomListPresenter(matrixClient),
onRoomClicked = {
backstack.push(NavTarget.Messages(it))
})
is NavTarget.Messages -> viewModelSupportNode(buildContext) {
MessagesScreen(
roomId = navTarget.roomId.value,

View file

@ -4,6 +4,7 @@ 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.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
@ -13,6 +14,7 @@ import io.element.android.x.core.di.viewModelSupportNode
import io.element.android.x.features.login.node.LoginFlowNode
import io.element.android.x.features.onboarding.OnBoardingScreen
import kotlinx.parcelize.Parcelize
import timber.log.Timber
class NotLoggedInFlowNode(
buildContext: BuildContext,
@ -25,6 +27,13 @@ class NotLoggedInFlowNode(
buildContext = buildContext
) {
init {
lifecycle.subscribe(
onCreate = { Timber.v("OnCreate") },
onDestroy = { Timber.v("OnDestroy") }
)
}
sealed interface NavTarget : Parcelable {
@Parcelize
object OnBoarding : NavTarget

View file

@ -25,6 +25,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.replace
import io.element.android.x.BuildConfig
import io.element.android.x.component.ShowkaseButton
@ -61,7 +62,7 @@ class RootFlowNode(
initialElement = NavTarget.SplashScreen,
savedStateMap = buildContext.savedStateMap,
),
private val daggerComponentOwner: DaggerComponentOwner,
private val appComponentOwner: DaggerComponentOwner,
private val matrix: Matrix,
private val sessionComponentsOwner: SessionComponentsOwner,
) :
@ -71,23 +72,32 @@ class RootFlowNode(
plugins = listOf(SessionComponentsOwnerInteractor(sessionComponentsOwner)),
),
DaggerComponentOwner by daggerComponentOwner {
DaggerComponentOwner by appComponentOwner {
init {
Timber.v("Init")
lifecycle.subscribe(
onCreate = { Timber.v("OnCreate") },
onDestroy = { Timber.v("OnDestroy") }
)
}
init {
matrix.isLoggedIn()
.distinctUntilChanged()
.onEach { isLoggedIn ->
Timber.v("IsLoggedIn")
if (isLoggedIn) {
val matrixClient = matrix.restoreSession()
if (matrixClient == null) {
backstack.replace(NavTarget.NotLoggedInFlow)
backstack.newRoot(NavTarget.NotLoggedInFlow)
} else {
matrixClient.startSync()
sessionComponentsOwner.create(matrixClient)
backstack.replace(NavTarget.LoggedInFlow(matrixClient.sessionId))
backstack.newRoot(NavTarget.LoggedInFlow(matrixClient.sessionId))
}
} else {
backstack.replace(NavTarget.NotLoggedInFlow)
backstack.newRoot(NavTarget.NotLoggedInFlow)
}
}
.launchIn(lifecycleScope)
@ -124,7 +134,10 @@ class RootFlowNode(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.LoggedInFlow -> LoggedInFlowNode(buildContext, navTarget.sessionId)
is NavTarget.LoggedInFlow -> {
val matrixClient = sessionComponentsOwner.activeSessionComponent!!.matrixClient()
LoggedInFlowNode(buildContext, navTarget.sessionId, matrixClient)
}
NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext)
NavTarget.SplashScreen -> node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {

View file

@ -2,10 +2,6 @@ package io.element.android.x.features.roomlist
import android.text.format.DateFormat
import android.text.format.DateUtils
import java.time.Period
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.math.absoluteValue
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
@ -14,8 +10,13 @@ import kotlinx.datetime.toInstant
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toLocalDateTime
import java.time.Period
import java.time.format.DateTimeFormatter
import java.util.Locale
import javax.inject.Inject
import kotlin.math.absoluteValue
class LastMessageFormatter(
class LastMessageFormatter @Inject constructor(
private val clock: Clock = Clock.System,
private val locale: Locale = Locale.getDefault()
) {

View file

@ -0,0 +1,29 @@
package io.element.android.x.features.roomlist
import androidx.compose.ui.platform.AndroidUiDispatcher
import androidx.lifecycle.lifecycleScope
import app.cash.molecule.RecompositionClock
import app.cash.molecule.launchMolecule
import com.bumble.appyx.core.node.Node
import io.element.android.x.core.architecture.Presenter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
inline fun <reified State, reified Event> Node.presenterConnector(presenter: Presenter<State, Event>): NodePresenterConnector<State, Event> {
return NodePresenterConnector(node = this, presenter = presenter)
}
class NodePresenterConnector<State, Event>(private val node: Node, presenter: Presenter<State, Event>) {
private val moleculeScope = CoroutineScope(node.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
private val eventFlow: MutableSharedFlow<Event> = MutableSharedFlow(extraBufferCapacity = 64)
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.ContextClock) {
presenter.present(events = eventFlow)
}
fun emitEvent(event: Event) {
eventFlow.tryEmit(event)
}
}

View file

@ -0,0 +1,43 @@
package io.element.android.x.features.roomlist
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.x.features.roomlist.model.RoomListScreen
import io.element.android.x.matrix.core.RoomId
class RoomListNode(
buildContext: BuildContext,
presenter: RoomListPresenter,
private val onRoomClicked: (RoomId) -> Unit
) : Node(buildContext) {
private val connector = presenterConnector(presenter)
private fun updateFilter(filter: String) {
connector.emitEvent(RoomListScreen.Event.UpdateFilter(filter))
}
private fun updateVisibleRange(range: IntRange) {
connector.emitEvent((RoomListScreen.Event.UpdateVisibleRange(range)))
}
private fun logout() {
connector.emitEvent(RoomListScreen.Event.Logout)
}
@Composable
override fun View(modifier: Modifier) {
val state by connector.stateFlow.collectAsState()
RoomListView(
state = state,
onRoomClicked = onRoomClicked,
onFilterChanged = this::updateFilter,
onScrollOver = this::updateVisibleRange,
onLogoutClicked = this::logout
)
}
}

View file

@ -0,0 +1,148 @@
package io.element.android.x.features.roomlist
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.x.core.architecture.Presenter
import io.element.android.x.core.coroutine.parallelMap
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.roomlist.model.MatrixUser
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders
import io.element.android.x.features.roomlist.model.RoomListScreen
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.RoomSummary
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
private const val extendedRangeSize = 40
class RoomListPresenter @Inject constructor(
private val client: MatrixClient,
private val lastMessageFormatter: LastMessageFormatter = LastMessageFormatter(),
) : Presenter<RoomListScreen.State, RoomListScreen.Event> {
@Composable
override fun present(events: Flow<RoomListScreen.Event>): RoomListScreen.State {
val matrixUser: MutableState<MatrixUser?> = remember {
mutableStateOf(null)
}
var filter by rememberSaveable { mutableStateOf("") }
val isLoginOut = rememberSaveable { mutableStateOf(false) }
val roomSummaries by client
.roomSummaryDataSource()
.roomSummaries()
.collectAsState(initial = null)
val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember {
mutableStateOf(persistentListOf())
}
LaunchedEffect(Unit) {
initialLoad(matrixUser)
events.collect { event ->
when (event) {
RoomListScreen.Event.Logout -> logout(isLoginOut)
is RoomListScreen.Event.UpdateFilter -> filter = event.newFilter
is RoomListScreen.Event.UpdateVisibleRange -> updateVisibleRange(event.range)
}
}
}
LaunchedEffect(roomSummaries, filter) {
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
}
return RoomListScreen.State(
matrixUser = matrixUser.value,
roomList = filteredRoomSummaries.value,
filter = filter,
isLoginOut = isLoginOut.value
)
}
private suspend fun updateFilteredRoomSummaries(roomSummaries: List<RoomSummary>?, filter: String): ImmutableList<RoomListRoomSummary> {
val mappedRoomSummaries = mapRoomSummaries(roomSummaries.orEmpty())
return if (filter.isEmpty()) {
mappedRoomSummaries
} else {
mappedRoomSummaries.filter { it.name.contains(filter, ignoreCase = true) }
}.toImmutableList()
}
private suspend fun initialLoad(matrixUser: MutableState<MatrixUser?>) {
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData =
loadAvatarData(
userDisplayName ?: client.userId().value,
userAvatarUrl,
AvatarSize.SMALL
)
matrixUser.value = MatrixUser(
username = userDisplayName ?: client.userId().value,
avatarUrl = userAvatarUrl,
avatarData = avatarData,
)
}
private suspend fun logout(isLoginOut: MutableState<Boolean>) {
isLoginOut.value = true
delay(2000)
client.logout()
isLoginOut.value = false
}
private fun updateVisibleRange(range: IntRange) {
if (range.isEmpty()) return
val midExtendedRangeSize = extendedRangeSize / 2
val extendedRangeStart = (range.first - midExtendedRangeSize).coerceAtLeast(0)
// Safe to give bigger size than room list
val extendedRangeEnd = range.last + midExtendedRangeSize
val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd)
client.roomSummaryDataSource().setSlidingSyncRange(extendedRange)
}
private suspend fun mapRoomSummaries(
roomSummaries: List<RoomSummary>
): List<RoomListRoomSummary> {
return roomSummaries.parallelMap { roomSummary ->
when (roomSummary) {
is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
is RoomSummary.Filled -> {
val avatarData = loadAvatarData(
roomSummary.details.name,
roomSummary.details.avatarURLString
)
RoomListRoomSummary(
id = roomSummary.identifier(),
name = roomSummary.details.name,
hasUnread = roomSummary.details.unreadNotificationCount > 0,
timestamp = lastMessageFormatter.format(roomSummary.details.lastMessageTimestamp),
lastMessage = roomSummary.details.lastMessage,
avatarData = avatarData,
)
}
}
}
}
private suspend fun loadAvatarData(
name: String,
url: String?,
size: AvatarSize = AvatarSize.MEDIUM
): AvatarData {
val model = client.mediaResolver()
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
}

View file

@ -5,7 +5,6 @@ package io.element.android.x.features.roomlist
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
@ -21,10 +20,6 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Velocity
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.designsystem.ElementXTheme
import io.element.android.x.designsystem.components.ProgressDialog
@ -33,41 +28,35 @@ import io.element.android.x.features.roomlist.components.RoomListTopBar
import io.element.android.x.features.roomlist.components.RoomSummaryRow
import io.element.android.x.features.roomlist.model.MatrixUser
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListViewState
import io.element.android.x.features.roomlist.model.RoomListScreen
import io.element.android.x.features.roomlist.model.stubbedRoomSummaries
import io.element.android.x.matrix.core.RoomId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun RoomListScreen(
viewModel: RoomListViewModel = mavericksViewModel(),
onSuccessLogout: () -> Unit = { },
onRoomClicked: (RoomId) -> Unit = { }
fun RoomListView(
state: RoomListScreen.State,
modifier: Modifier = Modifier,
onRoomClicked: (RoomId) -> Unit = {},
onFilterChanged: (String) -> Unit = {},
onLogoutClicked: () -> Unit = {},
onScrollOver: (IntRange) -> Unit = {},
) {
val logoutAction by viewModel.collectAsState(RoomListViewState::logoutAction)
val filter by viewModel.collectAsState(RoomListViewState::filter)
if (logoutAction is Success) {
onSuccessLogout()
return
}
LogCompositions(tag = "RoomListScreen", msg = "Root")
val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms)
val matrixUser by viewModel.collectAsState(RoomListViewState::user)
RoomListContent(
roomSummaries = roomSummaries().orEmpty().toImmutableList(),
matrixUser = matrixUser(),
RoomListView(
roomSummaries = state.roomList,
matrixUser = state.matrixUser,
filter = state.filter,
isLoginOut = state.isLoginOut,
modifier = modifier,
onRoomClicked = onRoomClicked,
onLogoutClicked = viewModel::logout,
isLoginOut = logoutAction is Loading,
filter = filter,
onFilterChanged = viewModel::filterRoom,
onScrollOver = viewModel::updateVisibleRange
onFilterChanged = onFilterChanged,
onLogoutClicked = onLogoutClicked,
onScrollOver = onScrollOver
)
}
@Composable
fun RoomListContent(
fun RoomListView(
roomSummaries: ImmutableList<RoomListRoomSummary>,
matrixUser: MatrixUser?,
filter: String,
@ -141,15 +130,11 @@ fun RoomListContent(
private fun RoomListRoomSummary.contentType() = isPlaceholder
private fun LazyListState.isScrolled(): Boolean {
return firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0
}
@Preview
@Composable
fun PreviewableRoomListContent() {
fun PreviewableRoomListView() {
ElementXTheme(darkTheme = false) {
RoomListContent(
RoomListView(
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
@ -164,9 +149,9 @@ fun PreviewableRoomListContent() {
@Preview
@Composable
fun PreviewableDarkRoomListContent() {
fun PreviewableDarkRoomListView() {
ElementXTheme(darkTheme = true) {
RoomListContent(
RoomListView(
roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {},

View file

@ -1,159 +0,0 @@
package io.element.android.x.features.roomlist
import com.airbnb.mvrx.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.coroutine.parallelMap
import io.element.android.x.core.di.daggerMavericksViewModelFactory
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.di.SessionScope
import io.element.android.x.features.roomlist.model.MatrixUser
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders
import io.element.android.x.features.roomlist.model.RoomListViewState
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.RoomSummary
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
private const val extendedRangeSize = 40
@ContributesViewModel(SessionScope::class)
class RoomListViewModel @AssistedInject constructor(
private val client: MatrixClient,
@Assisted initialState: RoomListViewState
) :
MavericksViewModel<RoomListViewState>(initialState) {
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by daggerMavericksViewModelFactory()
private val lastMessageFormatter = LastMessageFormatter()
init {
handleInit()
}
fun logout() {
viewModelScope.launch {
suspend {
delay(2000)
client.logout()
}.execute {
copy(logoutAction = it)
}
}
}
fun filterRoom(filter: String) {
setState {
copy(
filter = filter
)
}
}
fun updateVisibleRange(range: IntRange) {
viewModelScope.launch {
if (range.isEmpty()) return@launch
val midExtendedRangeSize = extendedRangeSize / 2
val extendedRangeStart = (range.first - midExtendedRangeSize).coerceAtLeast(0)
// Safe to give bigger size than room list
val extendedRangeEnd = range.last + midExtendedRangeSize
val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd)
client.roomSummaryDataSource().setSlidingSyncRange(extendedRange)
}
}
private fun handleInit() {
suspend {
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData =
loadAvatarData(
userDisplayName ?: client.userId().value,
userAvatarUrl,
AvatarSize.SMALL
)
MatrixUser(
username = userDisplayName ?: client.userId().value,
avatarUrl = userAvatarUrl,
avatarData = avatarData,
)
}.execute {
copy(user = it)
}
// Observe the room list and the filter
combine(
client.roomSummaryDataSource().roomSummaries()
.map(::mapRoomSummaries)
.flowOn(Dispatchers.Default),
stateFlow
.map { it.filter }
.distinctUntilChanged(),
) { list, filter ->
if (filter.isEmpty()) {
list
} else {
list.filter { it.name.contains(filter, ignoreCase = true) }
}
}
.execute {
copy(
rooms = when {
it is Loading ||
// Note: this second case will prevent to handle correctly the empty case
(it is Success && it().isEmpty() && filter.isEmpty()) -> {
// Show fake placeholders to avoid having empty screen
Loading(RoomListRoomSummaryPlaceholders.createFakeList(size = 16))
}
else -> {
it
}
}
)
}
}
private suspend fun mapRoomSummaries(
roomSummaries: List<RoomSummary>
): List<RoomListRoomSummary> {
return roomSummaries.parallelMap { roomSummary ->
when (roomSummary) {
is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
is RoomSummary.Filled -> {
val avatarData = loadAvatarData(
roomSummary.details.name,
roomSummary.details.avatarURLString
)
RoomListRoomSummary(
id = roomSummary.identifier(),
name = roomSummary.details.name,
hasUnread = roomSummary.details.unreadNotificationCount > 0,
timestamp = lastMessageFormatter.format(roomSummary.details.lastMessageTimestamp),
lastMessage = roomSummary.details.lastMessage,
avatarData = avatarData,
)
}
}
}
}
private suspend fun loadAvatarData(
name: String,
url: String?,
size: AvatarSize = AvatarSize.MEDIUM
): AvatarData {
val model = client.mediaResolver()
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
}

View file

@ -0,0 +1,21 @@
package io.element.android.x.features.roomlist.model
import androidx.compose.runtime.Stable
import kotlinx.collections.immutable.ImmutableList
object RoomListScreen {
@Stable
data class State(
val matrixUser: MatrixUser?,
val roomList: ImmutableList<RoomListRoomSummary>,
val filter: String,
val isLoginOut: Boolean,
)
sealed interface Event {
object Logout : Event
data class UpdateFilter(val newFilter: String) : Event
data class UpdateVisibleRange(val range: IntRange): Event
}
}

View file

@ -1,16 +0,0 @@
package io.element.android.x.features.roomlist.model
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import io.element.android.x.matrix.core.RoomId
data class RoomListViewState(
val user: Async<MatrixUser> = Uninitialized,
// Will contain the filtered rooms, using ::filter (if filter is not empty)
val rooms: Async<List<RoomListRoomSummary>> = Uninitialized,
val filter: String = "",
val canLoadMore: Boolean = false,
val logoutAction: Async<Unit> = Uninitialized,
val roomsById: Map<RoomId, RoomListRoomSummary> = emptyMap()
) : MavericksState

View file

@ -0,0 +1,9 @@
package io.element.android.x.core.architecture
import androidx.compose.runtime.Composable
import kotlinx.coroutines.flow.Flow
interface Presenter<State, Event> {
@Composable
fun present(events: Flow<Event>): State
}