Show badges for new invites (#355)
Show badges for new invites Closes #238
This commit is contained in:
parent
6447e4fe36
commit
2171f1a8ab
49 changed files with 744 additions and 153 deletions
1
changelog.d/238.feature
Normal file
1
changelog.d/238.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[Create and join rooms] New invites are now marked with a badge
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitelist.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface SeenInvitesStore {
|
||||||
|
fun seenRoomIds(): Flow<Set<RoomId>>
|
||||||
|
suspend fun markAsSeen(roomIds: Set<RoomId>)
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.invitelist.api)
|
api(projects.features.invitelist.api)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
@ -48,6 +49,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.features.invitelist.test)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitelist.impl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_seeninvites")
|
||||||
|
private val seenInvitesKey = stringSetPreferencesKey("seenInvites")
|
||||||
|
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultSeenInvitesStore @Inject constructor(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
) : SeenInvitesStore {
|
||||||
|
|
||||||
|
private val store = context.dataStore
|
||||||
|
|
||||||
|
override fun seenRoomIds(): Flow<Set<RoomId>> =
|
||||||
|
store.data.map { prefs ->
|
||||||
|
prefs[seenInvitesKey]
|
||||||
|
.orEmpty()
|
||||||
|
.map { RoomId(it) }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun markAsSeen(roomIds: Set<RoomId>) {
|
||||||
|
store.edit { prefs ->
|
||||||
|
prefs[seenInvitesKey] = roomIds.map { it.value }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -17,12 +17,15 @@
|
||||||
package io.element.android.features.invitelist.impl
|
package io.element.android.features.invitelist.impl
|
||||||
|
|
||||||
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.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -34,11 +37,13 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummary
|
import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class InviteListPresenter @Inject constructor(
|
class InviteListPresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
|
private val store: SeenInvitesStore,
|
||||||
) : Presenter<InviteListState> {
|
) : Presenter<InviteListState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -48,6 +53,21 @@ class InviteListPresenter @Inject constructor(
|
||||||
.roomSummaries()
|
.roomSummaries()
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
|
|
||||||
|
var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
seenInvites = store.seenRoomIds().first()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(invites) {
|
||||||
|
store.markAsSeen(
|
||||||
|
invites
|
||||||
|
.filterIsInstance<RoomSummary.Filled>()
|
||||||
|
.map { it.details.roomId }
|
||||||
|
.toSet()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val acceptedAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
val acceptedAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||||
val declinedAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
val declinedAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||||
|
|
@ -86,8 +106,17 @@ class InviteListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val inviteList = remember(seenInvites, invites) {
|
||||||
|
invites
|
||||||
|
.filterIsInstance<RoomSummary.Filled>()
|
||||||
|
.map {
|
||||||
|
it.toInviteSummary(seenInvites.contains(it.details.roomId))
|
||||||
|
}
|
||||||
|
.toPersistentList()
|
||||||
|
}
|
||||||
|
|
||||||
return InviteListState(
|
return InviteListState(
|
||||||
inviteList = invites.mapNotNull(::toInviteSummary).toPersistentList(),
|
inviteList = inviteList,
|
||||||
declineConfirmationDialog = decliningInvite.value?.let {
|
declineConfirmationDialog = decliningInvite.value?.let {
|
||||||
InviteDeclineConfirmationDialog.Visible(
|
InviteDeclineConfirmationDialog.Visible(
|
||||||
isDirect = it.isDirect,
|
isDirect = it.isDirect,
|
||||||
|
|
@ -113,49 +142,44 @@ class InviteListPresenter @Inject constructor(
|
||||||
}.execute(declinedAction)
|
}.execute(declinedAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toInviteSummary(roomSummary: RoomSummary): InviteListInviteSummary? {
|
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
|
||||||
return when (roomSummary) {
|
val i = inviter
|
||||||
is RoomSummary.Filled -> roomSummary.details.run {
|
val avatarData = if (isDirect && i != null)
|
||||||
val i = inviter
|
AvatarData(
|
||||||
val avatarData = if (isDirect && i != null)
|
id = i.userId.value,
|
||||||
AvatarData(
|
name = i.displayName,
|
||||||
id = i.userId.value,
|
url = i.avatarUrl,
|
||||||
name = i.displayName,
|
)
|
||||||
url = i.avatarUrl,
|
else
|
||||||
)
|
AvatarData(
|
||||||
else
|
id = roomId.value,
|
||||||
AvatarData(
|
name = name,
|
||||||
id = roomId.value,
|
url = avatarURLString
|
||||||
name = name,
|
)
|
||||||
url = avatarURLString
|
|
||||||
)
|
|
||||||
|
|
||||||
val alias = if (isDirect)
|
val alias = if (isDirect)
|
||||||
inviter?.userId?.value
|
inviter?.userId?.value
|
||||||
else
|
else
|
||||||
canonicalAlias
|
canonicalAlias
|
||||||
|
|
||||||
InviteListInviteSummary(
|
InviteListInviteSummary(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
roomName = name,
|
roomName = name,
|
||||||
roomAlias = alias,
|
roomAlias = alias,
|
||||||
roomAvatarData = avatarData,
|
roomAvatarData = avatarData,
|
||||||
isDirect = isDirect,
|
isDirect = isDirect,
|
||||||
sender = if (isDirect) null else inviter?.let {
|
isNew = !seen,
|
||||||
InviteSender(
|
sender = if (isDirect) null else inviter?.run {
|
||||||
userId = it.userId,
|
InviteSender(
|
||||||
displayName = it.displayName ?: "",
|
userId = userId,
|
||||||
avatarData = AvatarData(
|
displayName = displayName ?: "",
|
||||||
id = it.userId.value,
|
avatarData = AvatarData(
|
||||||
name = it.displayName,
|
id = userId.value,
|
||||||
url = it.avatarUrl,
|
name = displayName,
|
||||||
),
|
url = avatarUrl,
|
||||||
)
|
),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.invitelist.impl.components
|
package io.element.android.features.invitelist.impl.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
|
@ -27,13 +28,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
import androidx.compose.foundation.text.appendInlineContent
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.Placeholder
|
import androidx.compose.ui.text.Placeholder
|
||||||
|
|
@ -59,6 +64,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
|
@ -73,8 +79,8 @@ internal fun InviteSummaryRow(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = minHeight)
|
.heightIn(min = minHeight)
|
||||||
) {
|
) {
|
||||||
DefaultInviteSummaryRow(
|
DefaultInviteSummaryRow(
|
||||||
invite = invite,
|
invite = invite,
|
||||||
|
|
@ -92,19 +98,20 @@ internal fun DefaultInviteSummaryRow(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.Top
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
invite.roomAvatarData,
|
invite.roomAvatarData,
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 12.dp, end = 4.dp)
|
.padding(start = 12.dp, end = 4.dp)
|
||||||
.alignByBaseline()
|
.alignByBaseline()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
// Name
|
// Name
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -152,6 +159,15 @@ internal fun DefaultInviteSummaryRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val unreadIndicatorColor = if (invite.isNew) MaterialTheme.roomListUnreadIndicator() else Color.Transparent
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(12.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(unreadIndicatorColor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,7 +199,11 @@ private fun SenderRow(sender: InviteSender) {
|
||||||
Placeholder(20.dp.toSp(), 20.dp.toSp(), PlaceholderVerticalAlign.Center)
|
Placeholder(20.dp.toSp(), 20.dp.toSp(), PlaceholderVerticalAlign.Center)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(Modifier.fillMaxHeight().padding(end = 4.dp)) {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
|
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ data class InviteListInviteSummary(
|
||||||
val roomAlias: String? = null,
|
val roomAlias: String? = null,
|
||||||
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName),
|
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName),
|
||||||
val sender: InviteSender? = null,
|
val sender: InviteSender? = null,
|
||||||
val isDirect: Boolean = false
|
val isDirect: Boolean = false,
|
||||||
|
val isNew: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class InviteSender(
|
data class InviteSender(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteList
|
||||||
aInviteListInviteSummary(),
|
aInviteListInviteSummary(),
|
||||||
aInviteListInviteSummary().copy(roomAlias = "#someroom:example.com"),
|
aInviteListInviteSummary().copy(roomAlias = "#someroom:example.com"),
|
||||||
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
|
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
|
||||||
|
aInviteListInviteSummary().copy(isNew = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
|
@ -50,7 +52,8 @@ class InviteListPresenterTests {
|
||||||
FakeMatrixClient(
|
FakeMatrixClient(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
),
|
||||||
|
FakeSeenInvitesStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -58,21 +61,7 @@ class InviteListPresenterTests {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
Truth.assertThat(initialState.inviteList).isEmpty()
|
Truth.assertThat(initialState.inviteList).isEmpty()
|
||||||
|
|
||||||
invitesDataSource.postRoomSummary(
|
invitesDataSource.postRoomSummary(listOf(aRoomSummary()))
|
||||||
listOf(
|
|
||||||
RoomSummary.Filled(
|
|
||||||
RoomSummaryDetails(
|
|
||||||
roomId = A_ROOM_ID,
|
|
||||||
name = A_ROOM_NAME,
|
|
||||||
avatarURLString = null,
|
|
||||||
isDirect = false,
|
|
||||||
lastMessage = null,
|
|
||||||
lastMessageTimestamp = null,
|
|
||||||
unreadNotificationCount = 0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val withInviteState = awaitItem()
|
val withInviteState = awaitItem()
|
||||||
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||||
|
|
@ -88,7 +77,8 @@ class InviteListPresenterTests {
|
||||||
FakeMatrixClient(
|
FakeMatrixClient(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
),
|
||||||
|
FakeSeenInvitesStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -117,7 +107,8 @@ class InviteListPresenterTests {
|
||||||
FakeMatrixClient(
|
FakeMatrixClient(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
),
|
||||||
|
FakeSeenInvitesStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -144,7 +135,8 @@ class InviteListPresenterTests {
|
||||||
FakeMatrixClient(
|
FakeMatrixClient(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
),
|
||||||
|
FakeSeenInvitesStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -169,7 +161,8 @@ class InviteListPresenterTests {
|
||||||
FakeMatrixClient(
|
FakeMatrixClient(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
),
|
||||||
|
FakeSeenInvitesStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -194,7 +187,8 @@ class InviteListPresenterTests {
|
||||||
FakeMatrixClient(
|
FakeMatrixClient(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
),
|
||||||
|
FakeSeenInvitesStore(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -219,7 +213,7 @@ class InviteListPresenterTests {
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client)
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -246,7 +240,7 @@ class InviteListPresenterTests {
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client)
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||||
val ex = Throwable("Ruh roh!")
|
val ex = Throwable("Ruh roh!")
|
||||||
room.givenRejectInviteResult(Result.failure(ex))
|
room.givenRejectInviteResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -278,7 +272,7 @@ class InviteListPresenterTests {
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client)
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||||
val ex = Throwable("Ruh roh!")
|
val ex = Throwable("Ruh roh!")
|
||||||
room.givenRejectInviteResult(Result.failure(ex))
|
room.givenRejectInviteResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -311,7 +305,7 @@ class InviteListPresenterTests {
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client)
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -335,7 +329,7 @@ class InviteListPresenterTests {
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client)
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||||
val ex = Throwable("Ruh roh!")
|
val ex = Throwable("Ruh roh!")
|
||||||
room.givenAcceptInviteResult(Result.failure(ex))
|
room.givenAcceptInviteResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -361,7 +355,7 @@ class InviteListPresenterTests {
|
||||||
invitesDataSource = invitesDataSource,
|
invitesDataSource = invitesDataSource,
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = InviteListPresenter(client)
|
val presenter = InviteListPresenter(client, FakeSeenInvitesStore())
|
||||||
val ex = Throwable("Ruh roh!")
|
val ex = Throwable("Ruh roh!")
|
||||||
room.givenAcceptInviteResult(Result.failure(ex))
|
room.givenAcceptInviteResult(Result.failure(ex))
|
||||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||||
|
|
@ -381,6 +375,71 @@ class InviteListPresenterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - stores seen invites when received`() = runTest {
|
||||||
|
val invitesDataSource = FakeRoomSummaryDataSource()
|
||||||
|
val store = FakeSeenInvitesStore()
|
||||||
|
val presenter = InviteListPresenter(
|
||||||
|
FakeMatrixClient(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
invitesDataSource = invitesDataSource,
|
||||||
|
),
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem()
|
||||||
|
|
||||||
|
// When one invite is received, that ID is saved
|
||||||
|
invitesDataSource.postRoomSummary(listOf(aRoomSummary()))
|
||||||
|
|
||||||
|
awaitItem()
|
||||||
|
Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
|
||||||
|
|
||||||
|
// When a second is added, both are saved
|
||||||
|
invitesDataSource.postRoomSummary(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
||||||
|
|
||||||
|
awaitItem()
|
||||||
|
Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
|
||||||
|
|
||||||
|
// When they're both dismissed, an empty set is saved
|
||||||
|
invitesDataSource.postRoomSummary(listOf())
|
||||||
|
|
||||||
|
awaitItem()
|
||||||
|
Truth.assertThat(store.getProvidedRoomIds()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - marks invite as new if they're unseen`() = runTest {
|
||||||
|
val invitesDataSource = FakeRoomSummaryDataSource()
|
||||||
|
val store = FakeSeenInvitesStore()
|
||||||
|
store.publishRoomIds(setOf(A_ROOM_ID))
|
||||||
|
val presenter = InviteListPresenter(
|
||||||
|
FakeMatrixClient(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
invitesDataSource = invitesDataSource,
|
||||||
|
),
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
awaitItem()
|
||||||
|
|
||||||
|
invitesDataSource.postRoomSummary(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
val withInviteState = awaitItem()
|
||||||
|
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(2)
|
||||||
|
Truth.assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||||
|
Truth.assertThat(withInviteState.inviteList[0].isNew).isFalse()
|
||||||
|
Truth.assertThat(withInviteState.inviteList[1].roomId).isEqualTo(A_ROOM_ID_2)
|
||||||
|
Truth.assertThat(withInviteState.inviteList[1].isNew).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun FakeRoomSummaryDataSource.withRoomInvitation(): FakeRoomSummaryDataSource {
|
private suspend fun FakeRoomSummaryDataSource.withRoomInvitation(): FakeRoomSummaryDataSource {
|
||||||
postRoomSummary(
|
postRoomSummary(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -438,4 +497,17 @@ class InviteListPresenterTests {
|
||||||
)
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
||||||
|
RoomSummaryDetails(
|
||||||
|
roomId = id,
|
||||||
|
name = A_ROOM_NAME,
|
||||||
|
avatarURLString = null,
|
||||||
|
isDirect = false,
|
||||||
|
lastMessage = null,
|
||||||
|
lastMessageTimestamp = null,
|
||||||
|
unreadNotificationCount = 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
features/invitelist/test/build.gradle.kts
Normal file
29
features/invitelist/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.invitelist.test"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
api(projects.features.invitelist.api)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invitelist.test
|
||||||
|
|
||||||
|
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
class FakeSeenInvitesStore : SeenInvitesStore {
|
||||||
|
|
||||||
|
private var existing = MutableStateFlow(emptySet<RoomId>())
|
||||||
|
private var provided: Set<RoomId>? = null
|
||||||
|
|
||||||
|
fun publishRoomIds(invites: Set<RoomId>) {
|
||||||
|
existing.value = invites
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProvidedRoomIds() = provided
|
||||||
|
|
||||||
|
override fun seenRoomIds(): Flow<Set<RoomId>> = existing
|
||||||
|
|
||||||
|
override suspend fun markAsSeen(roomIds: Set<RoomId>) {
|
||||||
|
provided = roomIds.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,7 @@ dependencies {
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.dateformatter.api)
|
implementation(projects.libraries.dateformatter.api)
|
||||||
|
implementation(projects.features.invitelist.api)
|
||||||
implementation(projects.features.networkmonitor.api)
|
implementation(projects.features.networkmonitor.api)
|
||||||
implementation(libs.accompanist.placeholder)
|
implementation(libs.accompanist.placeholder)
|
||||||
api(projects.features.roomlist.api)
|
api(projects.features.roomlist.api)
|
||||||
|
|
@ -61,8 +62,10 @@ dependencies {
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.dateformatter.test)
|
testImplementation(projects.libraries.dateformatter.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
|
||||||
testImplementation(projects.libraries.permissions.noop)
|
testImplementation(projects.libraries.permissions.noop)
|
||||||
|
testImplementation(projects.features.invitelist.test)
|
||||||
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.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 com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultInviteStateDataSource @Inject constructor(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : InviteStateDataSource {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun inviteState(): InvitesState {
|
||||||
|
val invites by client
|
||||||
|
.invitesDataSource
|
||||||
|
.roomSummaries()
|
||||||
|
.collectAsState()
|
||||||
|
|
||||||
|
val seenInvites by seenInvitesStore
|
||||||
|
.seenRoomIds()
|
||||||
|
.collectAsState(initial = emptySet())
|
||||||
|
|
||||||
|
var state by remember { mutableStateOf(InvitesState.NoInvites) }
|
||||||
|
|
||||||
|
LaunchedEffect(invites, seenInvites) {
|
||||||
|
withContext(coroutineDispatchers.computation) {
|
||||||
|
state = when {
|
||||||
|
invites.isEmpty() -> InvitesState.NoInvites
|
||||||
|
seenInvites.containsAll(invites.roomIds) -> InvitesState.SeenInvites
|
||||||
|
else -> InvitesState.NewInvites
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val List<RoomSummary>.roomIds: Collection<RoomId>
|
||||||
|
get() = filterIsInstance<RoomSummary.Filled>().map { it.details.roomId }
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
interface InviteStateDataSource {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun inviteState(): InvitesState
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
private val inviteStateDataSource: InviteStateDataSource,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -86,13 +87,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
initialLoad(matrixUser)
|
initialLoad(matrixUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
val invites by client
|
|
||||||
.invitesDataSource
|
|
||||||
.roomSummaries()
|
|
||||||
.collectAsState()
|
|
||||||
|
|
||||||
Timber.v("Invites size = ${invites.size}")
|
|
||||||
|
|
||||||
// Session verification status (unknown, not verified, verified)
|
// Session verification status (unknown, not verified, verified)
|
||||||
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
||||||
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
|
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -112,7 +106,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
if (displaySearchResults) {
|
if (displaySearchResults) {
|
||||||
filter = ""
|
filter = ""
|
||||||
}
|
}
|
||||||
displaySearchResults =! displaySearchResults
|
displaySearchResults = !displaySearchResults
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +130,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
displayVerificationPrompt = displayVerificationPrompt,
|
displayVerificationPrompt = displayVerificationPrompt,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
displayInvites = invites.isNotEmpty(),
|
invitesState = inviteStateDataSource.inviteState(),
|
||||||
displaySearchResults = displaySearchResults,
|
displaySearchResults = displaySearchResults,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,13 @@ data class RoomListState(
|
||||||
val displayVerificationPrompt: Boolean,
|
val displayVerificationPrompt: Boolean,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val displayInvites: Boolean,
|
val invitesState: InvitesState,
|
||||||
val displaySearchResults: Boolean,
|
val displaySearchResults: Boolean,
|
||||||
val eventSink: (RoomListEvents) -> Unit
|
val eventSink: (RoomListEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class InvitesState {
|
||||||
|
NoInvites,
|
||||||
|
SeenInvites,
|
||||||
|
NewInvites,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState().copy(displayVerificationPrompt = true),
|
aRoomListState().copy(displayVerificationPrompt = true),
|
||||||
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
|
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
|
||||||
aRoomListState().copy(hasNetworkConnection = false),
|
aRoomListState().copy(hasNetworkConnection = false),
|
||||||
aRoomListState().copy(displayInvites = true),
|
aRoomListState().copy(invitesState = InvitesState.SeenInvites),
|
||||||
|
aRoomListState().copy(invitesState = InvitesState.NewInvites),
|
||||||
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()),
|
||||||
aRoomListState().copy(displaySearchResults = true),
|
aRoomListState().copy(displaySearchResults = true),
|
||||||
)
|
)
|
||||||
|
|
@ -49,7 +50,7 @@ internal fun aRoomListState() = RoomListState(
|
||||||
hasNetworkConnection = true,
|
hasNetworkConnection = true,
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
displayVerificationPrompt = false,
|
displayVerificationPrompt = false,
|
||||||
displayInvites = false,
|
invitesState = InvitesState.NoInvites,
|
||||||
displaySearchResults = false,
|
displaySearchResults = false,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -206,19 +206,23 @@ fun RoomListContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.displayInvites) {
|
if (state.invitesState != InvitesState.NoInvites) {
|
||||||
item {
|
item {
|
||||||
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxSize()) {
|
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxSize()) {
|
||||||
TextButton(
|
TextButton(
|
||||||
content = {
|
content = {
|
||||||
Text(stringResource(StringR.string.action_invites_list))
|
Text(stringResource(StringR.string.action_invites_list))
|
||||||
Spacer(Modifier.size(8.dp))
|
|
||||||
Box(
|
if (state.invitesState == InvitesState.NewInvites) {
|
||||||
modifier = Modifier
|
Spacer(Modifier.size(8.dp))
|
||||||
.size(12.dp)
|
|
||||||
.clip(CircleShape)
|
Box(
|
||||||
.background(MaterialTheme.roomListUnreadIndicator())
|
modifier = Modifier
|
||||||
)
|
.size(12.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.roomListUnreadIndicator())
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClick = onInvitesClicked,
|
onClick = onInvitesClicked,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionClock
|
||||||
|
import app.cash.molecule.moleculeFlow
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
internal class DefaultInviteStateDataSourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits NoInvites state if invites list is empty`() = runTest {
|
||||||
|
val matrixDataSource = FakeRoomSummaryDataSource()
|
||||||
|
val client = FakeMatrixClient(invitesDataSource = matrixDataSource)
|
||||||
|
val seenStore = FakeSeenInvitesStore()
|
||||||
|
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
dataSource.inviteState()
|
||||||
|
}.test {
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits NewInvites state if unseen invite exists`() = runTest {
|
||||||
|
val matrixDataSource = FakeRoomSummaryDataSource()
|
||||||
|
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
||||||
|
val client = FakeMatrixClient(invitesDataSource = matrixDataSource)
|
||||||
|
val seenStore = FakeSeenInvitesStore()
|
||||||
|
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
dataSource.inviteState()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits NewInvites state if multiple invites exist and at least one is unseen`() = runTest {
|
||||||
|
val matrixDataSource = FakeRoomSummaryDataSource()
|
||||||
|
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
|
||||||
|
val client = FakeMatrixClient(invitesDataSource = matrixDataSource)
|
||||||
|
val seenStore = FakeSeenInvitesStore()
|
||||||
|
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
||||||
|
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
dataSource.inviteState()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits SeenInvites state if invite exists in seen store`() = runTest {
|
||||||
|
val matrixDataSource = FakeRoomSummaryDataSource()
|
||||||
|
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
||||||
|
val client = FakeMatrixClient(invitesDataSource = matrixDataSource)
|
||||||
|
val seenStore = FakeSeenInvitesStore()
|
||||||
|
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
||||||
|
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
dataSource.inviteState()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits new state in response to upstream events`() = runTest {
|
||||||
|
val matrixDataSource = FakeRoomSummaryDataSource()
|
||||||
|
val client = FakeMatrixClient(invitesDataSource = matrixDataSource)
|
||||||
|
val seenStore = FakeSeenInvitesStore()
|
||||||
|
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
dataSource.inviteState()
|
||||||
|
}.test {
|
||||||
|
// Initially there are no invites
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
||||||
|
|
||||||
|
// When a single invite is received, state should be NewInvites
|
||||||
|
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
||||||
|
skipItems(1)
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||||
|
|
||||||
|
// If that invite is marked as seen, then the state becomes SeenInvites
|
||||||
|
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
||||||
|
skipItems(1)
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
|
||||||
|
|
||||||
|
// Another new invite resets it to NewInvites
|
||||||
|
matrixDataSource.postRoomSummary(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
|
||||||
|
skipItems(1)
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||||
|
|
||||||
|
// All of the invites going away reverts to NoInvites
|
||||||
|
matrixDataSource.postRoomSummary(emptyList())
|
||||||
|
skipItems(1)
|
||||||
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
|
class FakeInviteDataSource(
|
||||||
|
private val flow: Flow<InvitesState> = flowOf()
|
||||||
|
) : InviteStateDataSource {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun inviteState(): InvitesState {
|
||||||
|
val state = flow.collectAsState(initial = InvitesState.NoInvites)
|
||||||
|
return state.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,10 +38,12 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class RoomListPresenterTests {
|
@OptIn(ExperimentalCoroutinesApi::class) class RoomListPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - should start with no user and then load user with success`() = runTest {
|
fun `present - should start with no user and then load user with success`() = runTest {
|
||||||
|
|
@ -52,6 +54,7 @@ class RoomListPresenterTests {
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
FakeNetworkMonitor(),
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
|
FakeInviteDataSource(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -79,6 +82,7 @@ class RoomListPresenterTests {
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
FakeNetworkMonitor(),
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
|
FakeInviteDataSource(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -99,6 +103,7 @@ class RoomListPresenterTests {
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
FakeNetworkMonitor(),
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
|
FakeInviteDataSource(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -127,6 +132,7 @@ class RoomListPresenterTests {
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
FakeNetworkMonitor(),
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
|
FakeInviteDataSource(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -158,6 +164,7 @@ class RoomListPresenterTests {
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
FakeNetworkMonitor(),
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
|
FakeInviteDataSource(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -195,6 +202,7 @@ class RoomListPresenterTests {
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
FakeNetworkMonitor(),
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
|
FakeInviteDataSource(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -245,6 +253,7 @@ class RoomListPresenterTests {
|
||||||
},
|
},
|
||||||
FakeNetworkMonitor(),
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
|
FakeInviteDataSource(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -258,31 +267,34 @@ class RoomListPresenterTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - displays invites row if any invites exist`() = runTest {
|
fun `present - sets invite state`() = runTest {
|
||||||
val invitesDataSource = FakeRoomSummaryDataSource()
|
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
|
||||||
val presenter = RoomListPresenter(
|
val presenter = RoomListPresenter(
|
||||||
FakeMatrixClient(
|
FakeMatrixClient(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
invitesDataSource = invitesDataSource
|
|
||||||
),
|
),
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
FakeRoomLastMessageFormatter(),
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
FakeNetworkMonitor(),
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
|
FakeInviteDataSource(inviteStateFlow),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
Truth.assertThat(awaitItem().displayInvites).isFalse()
|
Truth.assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites)
|
||||||
|
|
||||||
invitesDataSource.postRoomSummary(listOf(aRoomSummaryFilled()))
|
inviteStateFlow.value = InvitesState.SeenInvites
|
||||||
Truth.assertThat(awaitItem().displayInvites).isTrue()
|
Truth.assertThat(awaitItem().invitesState).isEqualTo(InvitesState.SeenInvites)
|
||||||
|
|
||||||
invitesDataSource.postRoomSummary(listOf())
|
inviteStateFlow.value = InvitesState.NewInvites
|
||||||
Truth.assertThat(awaitItem().displayInvites).isFalse()
|
Truth.assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NewInvites)
|
||||||
|
|
||||||
|
inviteStateFlow.value = InvitesState.NoInvites
|
||||||
|
Truth.assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.androidx.preference)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrix.impl)
|
implementation(projects.libraries.matrix.impl)
|
||||||
implementation(projects.libraries.permissions.noop)
|
implementation(projects.libraries.permissions.noop)
|
||||||
|
|
@ -54,6 +55,7 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.dateformatter.impl)
|
implementation(projects.libraries.dateformatter.impl)
|
||||||
|
implementation(projects.features.invitelist.impl)
|
||||||
implementation(projects.features.roomlist.impl)
|
implementation(projects.features.roomlist.impl)
|
||||||
implementation(projects.features.login.impl)
|
implementation(projects.features.login.impl)
|
||||||
implementation(projects.features.networkmonitor.impl)
|
implementation(projects.features.networkmonitor.impl)
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,26 @@ import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.features.invitelist.impl.DefaultSeenInvitesStore
|
||||||
import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl
|
import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl
|
||||||
|
import io.element.android.features.roomlist.impl.DefaultInviteStateDataSource
|
||||||
import io.element.android.features.roomlist.impl.DefaultRoomLastMessageFormatter
|
import io.element.android.features.roomlist.impl.DefaultRoomLastMessageFormatter
|
||||||
import io.element.android.features.roomlist.impl.RoomListPresenter
|
import io.element.android.features.roomlist.impl.RoomListPresenter
|
||||||
import io.element.android.features.roomlist.impl.RoomListView
|
import io.element.android.features.roomlist.impl.RoomListView
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
||||||
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class RoomListScreen(
|
class RoomListScreen(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -52,6 +58,16 @@ class RoomListScreen(
|
||||||
sessionVerificationService = sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
networkMonitor = NetworkMonitorImpl(context),
|
networkMonitor = NetworkMonitorImpl(context),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
|
inviteStateDataSource = DefaultInviteStateDataSource(
|
||||||
|
matrixClient,
|
||||||
|
DefaultSeenInvitesStore(context),
|
||||||
|
CoroutineDispatchers(
|
||||||
|
io = Dispatchers.IO,
|
||||||
|
computation = Dispatchers.Default,
|
||||||
|
main = Dispatchers.Main,
|
||||||
|
diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3b63f562e8b5360a66546fdd6e77de9a23530338b0e1a031c5c2d052f585985b
|
oid sha256:a93f55fcc13131a84047147626b21abccc253c3e7e131afd56f85dfed783563f
|
||||||
size 48392
|
size 48527
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9f6d775fe3ecffe0c60b931e6d951a7da20d9832d940076d972821d268c58361
|
oid sha256:db30fa092d0d6d2158ec0f6d5fe1ca9b2707e648f0067e431584aa9f721a04a9
|
||||||
size 59646
|
size 59721
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a57b775eaad1b6e211d47588048c3722ec76083ca515ccface863b15ee497b5b
|
oid sha256:71e7579a781fb092c14614b86736d16f168fd34e8cd30937b899601285bc5db9
|
||||||
size 34216
|
size 34299
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:39ce49cd2193bcf2439845835c469ce5e6b0304536ba403f679e0ea76e77a85c
|
||||||
|
size 49400
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:682acde00a6cbf13aa356ff4ec91368b11eae9fa2f8750d1590ceea334045ebd
|
oid sha256:eef4fb6ed40c5b81737c967150694f2c7e3b3ecb09f50dabee92596ec80fc6b8
|
||||||
size 47079
|
size 47082
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:237e3727252f29ec3953fb0a27d6cee741cd5b485c9466e81799315f3852f0c7
|
oid sha256:566de16a101483d6bca48ee95037699e56c2b8b0fb5161ddbde4cbbc30f0e08c
|
||||||
size 58267
|
size 58261
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f2a733da2b24280b2d096e458a8bf470f2f8279008cc823553eee1072e9e3df5
|
oid sha256:0998c041de2ca2f7ccfd8c6fa3bc14201610cd66ce79b1e17be6b1d0c62f98e9
|
||||||
size 33238
|
size 33275
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c2a14823ece6aaf58128b3615e1c012dd906ae58b5ff52cb2b0841b49e43361d
|
||||||
|
size 47924
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e4e085e53b63f83a51bb726fc3f38f37c8fb6412e5b8ed3ec893b24c87fa63a4
|
oid sha256:fc812d7b9418a67040d8bd9a007444d611c00108662064baf79ae5805432f081
|
||||||
size 56723
|
size 56695
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e8e44bafcf2fd7119562d6a92a8deff74a7ba470b4a7a87951cf77e19eee2eb0
|
oid sha256:4031fc3820333ed5b28f2356792b957bc38cba1559b599b0ac0fef66074252e0
|
||||||
size 43576
|
size 43572
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b72ad15218e4f17b1c47018925194f3b21c70630bf8744395352707dc257ab70
|
oid sha256:8936f72285c8a0f483080a4ff007703e40f187abfe0f67bb3ae33eb57901a1af
|
||||||
size 44025
|
size 44022
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ac02a683f4a6297427e6e74bd331c45e9ee7efb044767568bc46566ae46f2274
|
oid sha256:6dd33c2e92caac62963ccc75c068aa2b27037fe075b48823d2a8b75c88b4f661
|
||||||
size 39618
|
size 39408
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ac02a683f4a6297427e6e74bd331c45e9ee7efb044767568bc46566ae46f2274
|
oid sha256:6dd33c2e92caac62963ccc75c068aa2b27037fe075b48823d2a8b75c88b4f661
|
||||||
size 39618
|
size 39408
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:151331de0ea941330ff611e312833aa9c731ed0cd49ed4f31f724dbb5c7ba252
|
oid sha256:e87dd78bba7e3fd682e647258d989e5d720b7a0e879d698f96611b98867cdaaf
|
||||||
size 55174
|
size 55009
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b8e00ba1ff01242bf3e1f4858cb66b5ed87681aec76090a545e8d729361e5e37
|
oid sha256:dbd348146e2161e9b2a57af31d0ad48c44dd5f0274fbebfd885ea628235e08f9
|
||||||
size 43034
|
size 42903
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:27b3164386391fc115d3ecdcf202495a3f03509c2c2c5564880ed756d56ed3b5
|
oid sha256:9c7eb8bd040fb4321ecf28b5e00619f5356f23e5b07f59feb3894dc6620b9643
|
||||||
size 43463
|
size 43335
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:64ec3959fddb9dccc271ac0fa28275fa43df893f8f672b99f9f594004cef438e
|
oid sha256:63e2e25f5e92c1890dd23ccc8f0e9c352587c0ac1190c012e430c21b7b8d728e
|
||||||
size 39052
|
size 38709
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:64ec3959fddb9dccc271ac0fa28275fa43df893f8f672b99f9f594004cef438e
|
oid sha256:63e2e25f5e92c1890dd23ccc8f0e9c352587c0ac1190c012e430c21b7b8d728e
|
||||||
size 39052
|
size 38709
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c2a23141c6cc8aa6e7e5f0757bda4d1117bf7f752412ccc4649ea560113b3e3f
|
oid sha256:6b6263f24f080bbf87704a8bf8784fdfab360c08fbf1cd048f82b2db84f7611d
|
||||||
size 39030
|
size 38738
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9095546c30bb5bc9800c852456fa9cd82d14e873a5e1488d29496af088e951da
|
oid sha256:c2a23141c6cc8aa6e7e5f0757bda4d1117bf7f752412ccc4649ea560113b3e3f
|
||||||
size 4882
|
size 39030
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8cfe321535e1ce223a0460435123dc59e74c436bdd8696cf4bdb2169f511832b
|
oid sha256:9095546c30bb5bc9800c852456fa9cd82d14e873a5e1488d29496af088e951da
|
||||||
size 28541
|
size 4882
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8cfe321535e1ce223a0460435123dc59e74c436bdd8696cf4bdb2169f511832b
|
||||||
|
size 28541
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d427b479f9eb6227bb92aecd997bab97a735ef85dd279a575d777e754effd258
|
oid sha256:fa79302cd00b188661ef35107645568741eb04ebde4d6d8f31c5c044ec5c2d6f
|
||||||
size 38639
|
size 38310
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d463ab6d045cc310973c5ff900cf4d9ae04e93cb1c7eac3f9b2aa0ea9b827cee
|
oid sha256:d427b479f9eb6227bb92aecd997bab97a735ef85dd279a575d777e754effd258
|
||||||
size 4815
|
size 38639
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0
|
oid sha256:d463ab6d045cc310973c5ff900cf4d9ae04e93cb1c7eac3f9b2aa0ea9b827cee
|
||||||
size 27758
|
size 4815
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:97b11203623c0c98da88dfedf85cb80d1f35cc55da570e12061b1385691bf1f0
|
||||||
|
size 27758
|
||||||
Loading…
Add table
Add a link
Reference in a new issue