Merge pull request #3631 from element-hq/feature/fga/rework_room_summary

Rework room summary
This commit is contained in:
ganfra 2024-10-09 11:44:45 +02:00 committed by GitHub
commit f43c92723b
70 changed files with 734 additions and 563 deletions

View file

@ -45,6 +45,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.getRoomInfoFlow
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -120,12 +122,9 @@ class RoomFlowNode @AssistedInject constructor(
} }
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) { private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
val roomInfoFlow = client.getRoomInfoFlow( val roomInfoFlow = client.getRoomInfoFlow(roomIdOrAlias = roomId.toRoomIdOrAlias())
roomId = roomId val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged()
).map { it.getOrNull() } val currentMembershipFlow = roomInfoFlow.map { it.getOrNull()?.currentUserMembership }.distinctUntilChanged()
val isSpaceFlow = roomInfoFlow.map { it?.isSpace.orFalse() }.distinctUntilChanged()
val currentMembershipFlow = roomInfoFlow.map { it?.currentUserMembership }.distinctUntilChanged()
combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace -> combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace ->
Timber.d("Room membership: $membership") Timber.d("Room membership: $membership")
when (membership) { when (membership) {

View file

@ -33,6 +33,8 @@ import io.element.android.libraries.core.meta.BuildMeta
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 io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.getRoomInfoFlow
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
@ -70,7 +72,7 @@ class JoinRoomPresenter @AssistedInject constructor(
override fun present(): JoinRoomState { override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var retryCount by remember { mutableIntStateOf(0) } var retryCount by remember { mutableIntStateOf(0) }
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty())
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val contentState by produceState<ContentState>( val contentState by produceState<ContentState>(
@ -204,7 +206,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
name = name, name = name,
topic = topic, topic = topic,
alias = canonicalAlias, alias = canonicalAlias,
numberOfMembers = activeMembersCount, numberOfMembers = activeMembersCount.toLong(),
isDm = isDm, isDm = isDm,
roomType = if (isSpace) RoomType.Space else RoomType.Room, roomType = if (isSpace) RoomType.Space else RoomType.Room,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,

View file

@ -19,7 +19,6 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> { open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {

View file

@ -32,8 +32,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -67,10 +67,10 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is joined then content state is filled with his data`() = runTest { fun `present - when room is joined then content state is filled with his data`() = runTest {
val roomInfo = aRoomInfo() val roomSummary = aRoomSummary()
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -81,22 +81,22 @@ class JoinRoomPresenterTest {
awaitItem().also { state -> awaitItem().also { state ->
val contentState = state.contentState as ContentState.Loaded val contentState = state.contentState as ContentState.Loaded
assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) assertThat(contentState.roomId).isEqualTo(A_ROOM_ID)
assertThat(contentState.name).isEqualTo(roomInfo.name) assertThat(contentState.name).isEqualTo(roomSummary.info.name)
assertThat(contentState.topic).isEqualTo(roomInfo.topic) assertThat(contentState.topic).isEqualTo(roomSummary.info.topic)
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias) assertThat(contentState.alias).isEqualTo(roomSummary.info.canonicalAlias)
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount) assertThat(contentState.numberOfMembers).isEqualTo(roomSummary.info.activeMembersCount)
assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect) assertThat(contentState.isDm).isEqualTo(roomSummary.info.isDirect)
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) assertThat(contentState.roomAvatarUrl).isEqualTo(roomSummary.info.avatarUrl)
} }
} }
} }
@Test @Test
fun `present - when room is invited then join authorization is equal to invited`() = runTest { fun `present - when room is invited then join authorization is equal to invited`() = runTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -114,13 +114,13 @@ class JoinRoomPresenterTest {
fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest { fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob") val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob")
val expectedInviteSender = inviter.toInviteSender() val expectedInviteSender = inviter.toInviteSender()
val roomInfo = aRoomInfo( val roomSummary = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED, currentUserMembership = CurrentUserMembership.INVITED,
inviter = inviter, inviter = inviter,
) )
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -140,10 +140,10 @@ class JoinRoomPresenterTest {
val acceptDeclinePresenter = Presenter { val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder) anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
} }
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -224,10 +224,10 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest { fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -243,10 +243,10 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest { fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(

View file

@ -31,6 +31,7 @@ import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.features.messages.impl.draft.ComposerDraftService import io.element.android.features.messages.impl.draft.ComposerDraftService
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor
import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.utils.TextPillificationHelper import io.element.android.features.messages.impl.utils.TextPillificationHelper

View file

@ -5,20 +5,22 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.features.messages.impl.messagecomposer package io.element.android.features.messages.impl.messagecomposer.suggestions
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
data class RoomAliasSuggestion( data class RoomAliasSuggestion(
val roomAlias: RoomAlias, val roomAlias: RoomAlias,
val roomSummary: RoomSummary, val roomId: RoomId,
val roomName: String?,
val roomAvatarUrl: String?,
) )
interface RoomAliasSuggestionsDataSource { interface RoomAliasSuggestionsDataSource {
@ -32,14 +34,16 @@ class DefaultRoomAliasSuggestionsDataSource @Inject constructor(
override fun getAllRoomAliasSuggestions(): Flow<List<RoomAliasSuggestion>> { override fun getAllRoomAliasSuggestions(): Flow<List<RoomAliasSuggestion>> {
return roomListService return roomListService
.allRooms .allRooms
.filteredSummaries .summaries
.map { roomSummaries -> .map { roomSummaries ->
roomSummaries roomSummaries
.mapNotNull { roomSummary -> .mapNotNull { roomSummary ->
roomSummary.canonicalAlias?.let { roomAlias -> roomSummary.info.canonicalAlias?.let { roomAlias ->
RoomAliasSuggestion( RoomAliasSuggestion(
roomAlias = roomAlias, roomAlias = roomAlias,
roomSummary = roomSummary, roomId = roomSummary.roomId,
roomName = roomSummary.info.name,
roomAvatarUrl = roomSummary.info.avatarUrl,
) )
} }
} }

View file

@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -60,7 +59,7 @@ fun SuggestionsPickerView(
when (suggestion) { when (suggestion) {
is ResolvedSuggestion.AtRoom -> "@room" is ResolvedSuggestion.AtRoom -> "@room"
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
is ResolvedSuggestion.Alias -> suggestion.roomSummary.roomId.value is ResolvedSuggestion.Alias -> suggestion.roomId.value
} }
} }
) { ) {
@ -96,12 +95,12 @@ private fun SuggestionItemView(
val avatarData = when (suggestion) { val avatarData = when (suggestion) {
is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize) is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize) is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize)
is ResolvedSuggestion.Alias -> suggestion.roomSummary.getAvatarData(avatarSize) is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize)
} }
val title = when (suggestion) { val title = when (suggestion) {
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title) is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
is ResolvedSuggestion.Alias -> suggestion.roomSummary.name is ResolvedSuggestion.Alias -> suggestion.roomName
} }
val subtitle = when (suggestion) { val subtitle = when (suggestion) {
is ResolvedSuggestion.AtRoom -> "@room" is ResolvedSuggestion.AtRoom -> "@room"
@ -152,11 +151,6 @@ internal fun SuggestionsPickerViewPreview() {
role = RoomMember.Role.USER, role = RoomMember.Role.USER,
) )
val anAlias = remember { RoomAlias("#room:domain.org") } val anAlias = remember { RoomAlias("#room:domain.org") }
val roomSummaryDetails = remember {
aRoomSummaryDetails(
name = "My room",
)
}
SuggestionsPickerView( SuggestionsPickerView(
roomId = RoomId("!room:matrix.org"), roomId = RoomId("!room:matrix.org"),
roomName = "Room", roomName = "Room",
@ -166,8 +160,10 @@ internal fun SuggestionsPickerViewPreview() {
ResolvedSuggestion.Member(roomMember), ResolvedSuggestion.Member(roomMember),
ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")), ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
ResolvedSuggestion.Alias( ResolvedSuggestion.Alias(
anAlias, roomAlias = anAlias,
roomSummaryDetails, roomId = RoomId("!room:matrix.org"),
roomName = "My room",
roomAvatarUrl = null,
) )
), ),
onSelectSuggestion = {} onSelectSuggestion = {}

View file

@ -7,7 +7,6 @@
package io.element.android.features.messages.impl.messagecomposer.suggestions package io.element.android.features.messages.impl.messagecomposer.suggestions
import io.element.android.features.messages.impl.messagecomposer.RoomAliasSuggestion
import io.element.android.libraries.core.data.filterUpTo import io.element.android.libraries.core.data.filterUpTo
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@ -55,7 +54,14 @@ class SuggestionsProcessor @Inject constructor() {
SuggestionType.Room -> { SuggestionType.Room -> {
roomAliasSuggestions roomAliasSuggestions
.filter { it.roomAlias.value.contains(suggestion.text, ignoreCase = true) } .filter { it.roomAlias.value.contains(suggestion.text, ignoreCase = true) }
.map { ResolvedSuggestion.Alias(it.roomAlias, it.roomSummary) } .map {
ResolvedSuggestion.Alias(
roomAlias = it.roomAlias,
roomId = it.roomId,
roomName = it.roomName,
roomAvatarUrl = it.roomAvatarUrl,
)
}
} }
SuggestionType.Command, SuggestionType.Command,
is SuggestionType.Custom -> { is SuggestionType.Custom -> {

View file

@ -9,6 +9,8 @@ package io.element.android.features.messages.impl.messagecomposer
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.messagecomposer.suggestions.DefaultRoomAliasSuggestionsDataSource
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestion
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
@ -38,7 +40,9 @@ class DefaultRoomAliasSuggestionsDataSourceTest {
listOf( listOf(
RoomAliasSuggestion( RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS, roomAlias = A_ROOM_ALIAS,
roomSummary = aRoomSummaryWithAnAlias roomId = aRoomSummaryWithAnAlias.roomId,
roomName = aRoomSummaryWithAnAlias.info.name,
roomAvatarUrl = aRoomSummaryWithAnAlias.info.avatarUrl
) )
) )
) )

View file

@ -7,6 +7,8 @@
package io.element.android.features.messages.impl.messagecomposer package io.element.android.features.messages.impl.messagecomposer
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestion
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow

View file

@ -8,7 +8,6 @@
package io.element.android.features.messages.impl.messagecomposer.suggestions package io.element.android.features.messages.impl.messagecomposer.suggestions
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.messagecomposer.RoomAliasSuggestion
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
@ -110,13 +109,25 @@ class SuggestionsProcessorTest {
val result = suggestionsProcessor.process( val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("ALI"), suggestion = aRoomSuggestion("ALI"),
roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()),
roomAliasSuggestions = listOf(RoomAliasSuggestion(A_ROOM_ALIAS, aRoomSummary)), roomAliasSuggestions = listOf(
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),
currentUserId = A_USER_ID, currentUserId = A_USER_ID,
canSendRoomMention = { true }, canSendRoomMention = { true },
) )
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
listOf( listOf(
ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary) ResolvedSuggestion.Alias(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
) )
) )
} }
@ -127,13 +138,25 @@ class SuggestionsProcessorTest {
val result = suggestionsProcessor.process( val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("ali"), suggestion = aRoomSuggestion("ali"),
roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()),
roomAliasSuggestions = listOf(RoomAliasSuggestion(A_ROOM_ALIAS, aRoomSummary)), roomAliasSuggestions = listOf(
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),
currentUserId = A_USER_ID, currentUserId = A_USER_ID,
canSendRoomMention = { true }, canSendRoomMention = { true },
) )
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
listOf( listOf(
ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary) ResolvedSuggestion.Alias(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
) )
) )
} }
@ -144,7 +167,14 @@ class SuggestionsProcessorTest {
val result = suggestionsProcessor.process( val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("tot"), suggestion = aRoomSuggestion("tot"),
roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()),
roomAliasSuggestions = listOf(RoomAliasSuggestion(A_ROOM_ALIAS, aRoomSummary)), roomAliasSuggestions = listOf(
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),
currentUserId = A_USER_ID, currentUserId = A_USER_ID,
canSendRoomMention = { true }, canSendRoomMention = { true },
) )

View file

@ -21,11 +21,12 @@ import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.getAvatarData
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@ -40,7 +41,6 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
private val notificationSettingsService: NotificationSettingsService, private val notificationSettingsService: NotificationSettingsService,
@Assisted private val isOneToOne: Boolean, @Assisted private val isOneToOne: Boolean,
private val roomListService: RoomListService, private val roomListService: RoomListService,
private val matrixClient: MatrixClient,
) : Presenter<EditDefaultNotificationSettingState> { ) : Presenter<EditDefaultNotificationSettingState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -57,8 +57,8 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val roomsWithUserDefinedMode: MutableState<List<RoomSummary>> = remember { val roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>> = remember {
mutableStateOf(listOf()) mutableStateOf(emptyList())
} }
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
@ -106,31 +106,37 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
.launchIn(this) .launchIn(this)
} }
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<RoomSummary>>) { private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>) {
roomListService.allRooms roomListService.allRooms
.summaries .summaries
.onEach { .onEach { roomSummaries ->
updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode) updateRoomsWithUserDefinedMode(roomSummaries, roomsWithUserDefinedMode)
} }
.launchIn(this) .launchIn(this)
} }
private fun CoroutineScope.updateRoomsWithUserDefinedMode( private suspend fun updateRoomsWithUserDefinedMode(
summaries: List<RoomSummary>, summaries: List<RoomSummary>,
roomsWithUserDefinedMode: MutableState<List<RoomSummary>> roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>
) = launch { ) {
val roomWithUserDefinedRules: Set<String> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrThrow().toSet() val roomWithUserDefinedRules: Set<String> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet()
roomsWithUserDefinedMode.value = summaries
val sortedSummaries = summaries .filter { roomSummary ->
.filterIsInstance<RoomSummary>() roomWithUserDefinedRules.contains(roomSummary.roomId.value) && roomSummary.isOneToOne == isOneToOne
.filter { }
val room = matrixClient.getRoom(it.roomId) ?: return@filter false .map { roomSummary ->
roomWithUserDefinedRules.contains(it.roomId.value) && isOneToOne == room.isOneToOne EditNotificationSettingRoomInfo(
roomId = roomSummary.roomId,
name = roomSummary.info.name,
heroesAvatar = roomSummary.info.heroes.map { hero ->
hero.getAvatarData(AvatarSize.CustomRoomNotificationSetting)
}.toImmutableList(),
avatarData = roomSummary.info.getAvatarData(AvatarSize.CustomRoomNotificationSetting),
notificationMode = roomSummary.info.userDefinedNotificationMode,
)
} }
// locale sensitive sorting // locale sensitive sorting
.sortedWith(compareBy(Collator.getInstance()) { it.name }) .sortedWith(compareBy(Collator.getInstance()) { roomSummary -> roomSummary.name })
roomsWithUserDefinedMode.value = sortedSummaries
} }
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch { private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch {

View file

@ -9,13 +9,12 @@ package io.element.android.features.preferences.impl.notifications.edit
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class EditDefaultNotificationSettingState( data class EditDefaultNotificationSettingState(
val isOneToOne: Boolean, val isOneToOne: Boolean,
val mode: RoomNotificationMode?, val mode: RoomNotificationMode?,
val roomsWithUserDefinedMode: ImmutableList<RoomSummary>, val roomsWithUserDefinedMode: ImmutableList<EditNotificationSettingRoomInfo>,
val changeNotificationSettingAction: AsyncAction<Unit>, val changeNotificationSettingAction: AsyncAction<Unit>,
val displayMentionsOnlyDisclaimer: Boolean, val displayMentionsOnlyDisclaimer: Boolean,
val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit,

View file

@ -9,9 +9,10 @@ package io.element.android.features.preferences.impl.notifications.edit
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> { open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
@ -33,21 +34,25 @@ private fun anEditDefaultNotificationSettingsState(
isOneToOne = isOneToOne, isOneToOne = isOneToOne,
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
roomsWithUserDefinedMode = persistentListOf( roomsWithUserDefinedMode = persistentListOf(
aRoomSummary("Room"), anEditNotificationSettingRoomInfo("Room"),
aRoomSummary(null), anEditNotificationSettingRoomInfo(null),
), ),
changeNotificationSettingAction = changeNotificationSettingAction, changeNotificationSettingAction = changeNotificationSettingAction,
displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer, displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer,
eventSink = {} eventSink = {}
) )
private fun aRoomSummary( private fun anEditNotificationSettingRoomInfo(
name: String?, name: String?,
) = aRoomSummaryDetails( ) = EditNotificationSettingRoomInfo(
roomId = RoomId("!roomId:domain"), roomId = RoomId("!roomId:domain"),
name = name, name = name,
avatarUrl = null, avatarData = AvatarData(
isDirect = false, id = "!roomId:domain",
lastMessage = null, name = name,
url = null,
size = AvatarSize.CustomRoomNotificationSetting,
),
heroesAvatar = persistentListOf(),
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
) )

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
@ -27,9 +26,7 @@ import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toPersistentList
/** /**
* A view that allows a user to edit the default notification setting for rooms. This can be set separately * A view that allows a user to edit the default notification setting for rooms. This can be set separately
@ -80,7 +77,7 @@ fun EditDefaultNotificationSettingView(
if (state.roomsWithUserDefinedMode.isNotEmpty()) { if (state.roomsWithUserDefinedMode.isNotEmpty()) {
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) { PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) {
state.roomsWithUserDefinedMode.forEach { summary -> state.roomsWithUserDefinedMode.forEach { summary ->
val subtitle = when (summary.userDefinedNotificationMode) { val subtitle = when (summary.notificationMode) {
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages) RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> { RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> {
stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords) stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
@ -101,10 +98,8 @@ fun EditDefaultNotificationSettingView(
}, },
leadingContent = ListItemContent.Custom { leadingContent = ListItemContent.Custom {
CompositeAvatar( CompositeAvatar(
avatarData = summary.getAvatarData(size = AvatarSize.CustomRoomNotificationSetting), avatarData = summary.avatarData,
heroes = summary.heroes.map { user -> heroes = summary.heroesAvatar,
user.getAvatarData(size = AvatarSize.CustomRoomNotificationSetting)
}.toPersistentList()
) )
}, },
onClick = { onClick = {

View file

@ -0,0 +1,21 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.preferences.impl.notifications.edit
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.room.RoomNotificationMode
import kotlinx.collections.immutable.ImmutableList
data class EditNotificationSettingRoomInfo(
val roomId: RoomId,
val name: String?,
val heroesAvatar: ImmutableList<AvatarData>,
val avatarData: AvatarData,
val notificationMode: RoomNotificationMode?
)

View file

@ -14,11 +14,8 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.awaitLastSequentialItem
@ -49,24 +46,20 @@ class EditDefaultNotificationSettingsPresenterTest {
@Test @Test
fun `present - ensure list of rooms with user defined mode`() = runTest { fun `present - ensure list of rooms with user defined mode`() = runTest {
val room = FakeMatrixRoom()
val notificationSettingsService = FakeNotificationSettingsService( val notificationSettingsService = FakeNotificationSettingsService(
initialRoomMode = RoomNotificationMode.ALL_MESSAGES, initialRoomMode = RoomNotificationMode.ALL_MESSAGES,
initialRoomModeIsDefault = false initialRoomModeIsDefault = false
) )
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService).apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService, matrixClient) val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
roomListService.postAllRooms(listOf(aRoomSummary(notificationMode = RoomNotificationMode.ALL_MESSAGES))) roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
val loadedState = consumeItemsUntilPredicate { state -> val loadedState = consumeItemsUntilPredicate { state ->
state.roomsWithUserDefinedMode.any { it.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES } state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }
}.last() }.last()
assertThat(loadedState.roomsWithUserDefinedMode.any { it.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue() assertThat(loadedState.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue()
} }
} }
@ -121,13 +114,11 @@ class EditDefaultNotificationSettingsPresenterTest {
private fun createEditDefaultNotificationSettingPresenter( private fun createEditDefaultNotificationSettingPresenter(
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
roomListService: FakeRoomListService = FakeRoomListService(), roomListService: FakeRoomListService = FakeRoomListService(),
matrixClient: FakeMatrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
): EditDefaultNotificationSettingPresenter { ): EditDefaultNotificationSettingPresenter {
return EditDefaultNotificationSettingPresenter( return EditDefaultNotificationSettingPresenter(
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
isOneToOne = false, isOneToOne = false,
roomListService = roomListService, roomListService = roomListService,
matrixClient = matrixClient
) )
} }
} }

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
@ -24,34 +25,35 @@ class RoomListRoomSummaryFactory @Inject constructor(
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter,
) { ) {
fun create(details: RoomSummary): RoomListRoomSummary { fun create(roomSummary: RoomSummary): RoomListRoomSummary {
val avatarData = details.getAvatarData(size = AvatarSize.RoomListItem) val roomInfo = roomSummary.info
val avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomListItem)
return RoomListRoomSummary( return RoomListRoomSummary(
id = details.roomId.value, id = roomSummary.roomId.value,
roomId = details.roomId, roomId = roomSummary.roomId,
name = details.name, name = roomInfo.name,
numberOfUnreadMessages = details.numUnreadMessages, numberOfUnreadMessages = roomInfo.numUnreadMessages,
numberOfUnreadMentions = details.numUnreadMentions, numberOfUnreadMentions = roomInfo.numUnreadMentions,
numberOfUnreadNotifications = details.numUnreadNotifications, numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
isMarkedUnread = details.isMarkedUnread, isMarkedUnread = roomInfo.isMarkedUnread,
timestamp = lastMessageTimestampFormatter.format(details.lastMessageTimestamp), timestamp = lastMessageTimestampFormatter.format(roomSummary.lastMessageTimestamp),
lastMessage = details.lastMessage?.let { message -> lastMessage = roomSummary.lastMessage?.let { message ->
roomLastMessageFormatter.format(message.event, details.isDm) roomLastMessageFormatter.format(message.event, roomInfo.isDm)
}.orEmpty(), }.orEmpty(),
avatarData = avatarData, avatarData = avatarData,
userDefinedNotificationMode = details.userDefinedNotificationMode, userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
hasRoomCall = details.hasRoomCall, hasRoomCall = roomInfo.hasRoomCall,
isDirect = details.isDirect, isDirect = roomInfo.isDirect,
isFavorite = details.isFavorite, isFavorite = roomInfo.isFavorite,
inviteSender = details.inviter?.toInviteSender(), inviteSender = roomInfo.inviter?.toInviteSender(),
isDm = details.isDm, isDm = roomInfo.isDm,
canonicalAlias = details.canonicalAlias, canonicalAlias = roomInfo.canonicalAlias,
displayType = if (details.currentUserMembership == CurrentUserMembership.INVITED) { displayType = if (roomInfo.currentUserMembership == CurrentUserMembership.INVITED) {
RoomSummaryDisplayType.INVITE RoomSummaryDisplayType.INVITE
} else { } else {
RoomSummaryDisplayType.ROOM RoomSummaryDisplayType.ROOM
}, },
heroes = details.heroes.map { user -> heroes = roomInfo.heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomListItem) user.getAvatarData(size = AvatarSize.RoomListItem)
}.toImmutableList(), }.toImmutableList(),
) )

View file

@ -22,9 +22,9 @@ data class RoomListRoomSummary(
val roomId: RoomId, val roomId: RoomId,
val name: String?, val name: String?,
val canonicalAlias: RoomAlias?, val canonicalAlias: RoomAlias?,
val numberOfUnreadMessages: Int, val numberOfUnreadMessages: Long,
val numberOfUnreadMentions: Int, val numberOfUnreadMentions: Long,
val numberOfUnreadNotifications: Int, val numberOfUnreadNotifications: Long,
val isMarkedUnread: Boolean, val isMarkedUnread: Boolean,
val timestamp: String?, val timestamp: String?,
val lastMessage: CharSequence?, val lastMessage: CharSequence?,

View file

@ -119,9 +119,9 @@ internal fun anInviteSender(
internal fun aRoomListRoomSummary( internal fun aRoomListRoomSummary(
id: String = "!roomId:domain", id: String = "!roomId:domain",
name: String? = "Room name", name: String? = "Room name",
numberOfUnreadMessages: Int = 0, numberOfUnreadMessages: Long = 0,
numberOfUnreadMentions: Int = 0, numberOfUnreadMentions: Long = 0,
numberOfUnreadNotifications: Int = 0, numberOfUnreadNotifications: Long = 0,
isMarkedUnread: Boolean = false, isMarkedUnread: Boolean = false,
lastMessage: String? = "Last message", lastMessage: String? = "Last message",
timestamp: String? = lastMessage?.let { "88:88" }, timestamp: String? = lastMessage?.let { "88:88" },

View file

@ -396,7 +396,7 @@ class RoomListPresenterTest {
val notificationSettingsService = FakeNotificationSettingsService() val notificationSettingsService = FakeNotificationSettingsService()
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(aRoomSummary(notificationMode = userDefinedMode))) roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode)))
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
notificationSettingsService = notificationSettingsService notificationSettingsService = notificationSettingsService

View file

@ -76,9 +76,9 @@ class RoomListRoomSummaryTest {
} }
internal fun createRoomListRoomSummary( internal fun createRoomListRoomSummary(
numberOfUnreadMentions: Int = 0, numberOfUnreadMentions: Long = 0,
numberOfUnreadMessages: Int = 0, numberOfUnreadMessages: Long = 0,
numberOfUnreadNotifications: Int = 0, numberOfUnreadNotifications: Long = 0,
isMarkedUnread: Boolean = false, isMarkedUnread: Boolean = false,
userDefinedNotificationMode: RoomNotificationMode? = null, userDefinedNotificationMode: RoomNotificationMode? = null,
isFavorite: Boolean = false, isFavorite: Boolean = false,

View file

@ -38,6 +38,8 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.io.Closeable import java.io.Closeable
import java.util.Optional import java.util.Optional
@ -94,7 +96,13 @@ interface MatrixClient : Closeable {
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
fun roomMembershipObserver(): RoomMembershipObserver fun roomMembershipObserver(): RoomMembershipObserver
fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>>
/**
* Get a room summary flow for a given room ID or alias.
* The flow will emit a new value whenever the room summary is updated.
* The flow will emit Optional.empty item if the room is not found.
*/
fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<RoomSummary>>
fun isMe(userId: UserId?) = userId == sessionId fun isMe(userId: UserId?) = userId == sessionId
@ -142,3 +150,14 @@ interface MatrixClient : Closeable {
fun canDeactivateAccount(): Boolean fun canDeactivateAccount(): Boolean
suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit> suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit>
} }
/**
* Get a room info flow for a given room ID or alias.
* The flow will emit a new value whenever the room info is updated.
* The flow will emit Optional.empty item if the room is not found.
*/
fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<MatrixRoomInfo>> {
return getRoomSummaryFlow(roomIdOrAlias)
.map { roomSummary -> roomSummary.map { it.info } }
.distinctUntilChanged()
}

View file

@ -33,6 +33,13 @@ data class MatrixRoomInfo(
val canonicalAlias: RoomAlias?, val canonicalAlias: RoomAlias?,
val alternativeAliases: ImmutableList<RoomAlias>, val alternativeAliases: ImmutableList<RoomAlias>,
val currentUserMembership: CurrentUserMembership, val currentUserMembership: CurrentUserMembership,
/**
* Member who invited the current user to a room that's in the invited
* state.
*
* Can be missing if the room membership invite event is missing from the
* store.
*/
val inviter: RoomMember?, val inviter: RoomMember?,
val activeMembersCount: Long, val activeMembersCount: Long,
val invitedMembersCount: Long, val invitedMembersCount: Long,
@ -43,7 +50,26 @@ data class MatrixRoomInfo(
val userDefinedNotificationMode: RoomNotificationMode?, val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean, val hasRoomCall: Boolean,
val activeRoomCallParticipants: ImmutableList<UserId>, val activeRoomCallParticipants: ImmutableList<UserId>,
val isMarkedUnread: Boolean,
/**
* "Interesting" messages received in that room, independently of the
* notification settings.
*/
val numUnreadMessages: Long,
/**
* Events that will notify the user, according to their
* notification settings.
*/
val numUnreadNotifications: Long,
/**
* Events causing mentions/highlights for the user, according to their
* notification settings.
*/
val numUnreadMentions: Long,
val heroes: ImmutableList<MatrixUser>, val heroes: ImmutableList<MatrixUser>,
val pinnedEventIds: ImmutableList<EventId>, val pinnedEventIds: ImmutableList<EventId>,
val creator: UserId?, val creator: UserId?,
) ) {
val aliases: List<RoomAlias>
get() = listOfNotNull(canonicalAlias) + alternativeAliases
}

View file

@ -7,35 +7,14 @@
package io.element.android.libraries.matrix.api.roomlist package io.element.android.libraries.matrix.api.roomlist
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
data class RoomSummary( data class RoomSummary(
val roomId: RoomId, val info: MatrixRoomInfo,
val name: String?,
val canonicalAlias: RoomAlias?,
val alternativeAliases: List<RoomAlias>,
val isDirect: Boolean,
val avatarUrl: String?,
val lastMessage: RoomMessage?, val lastMessage: RoomMessage?,
val numUnreadMessages: Int,
val numUnreadMentions: Int,
val numUnreadNotifications: Int,
val isMarkedUnread: Boolean,
val inviter: RoomMember?,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val isDm: Boolean,
val isFavorite: Boolean,
val currentUserMembership: CurrentUserMembership,
val heroes: List<MatrixUser>,
) { ) {
val roomId = info.id
val lastMessageTimestamp = lastMessage?.originServerTs val lastMessageTimestamp = lastMessage?.originServerTs
val aliases: List<RoomAlias> val isOneToOne get() = info.activeMembersCount == 2L
get() = listOfNotNull(canonicalAlias) + alternativeAliases
} }

View file

@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.InvitedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@ -84,8 +83,8 @@ import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -105,8 +104,8 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.Optional import java.util.Optional
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.INFINITE
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
@ -262,21 +261,14 @@ class RustMatrixClient(
* @param timeout the timeout to wait for the room to be available * @param timeout the timeout to wait for the room to be available
* @throws TimeoutCancellationException if the room is not available after the timeout * @throws TimeoutCancellationException if the room is not available after the timeout
*/ */
private suspend fun awaitJoinedRoom(roomIdOrAlias: RoomIdOrAlias, timeout: Duration): RoomSummary { private suspend fun awaitJoinedRoom(
val predicate: (List<RoomSummary>) -> Boolean = when (roomIdOrAlias) { roomIdOrAlias: RoomIdOrAlias,
is RoomIdOrAlias.Alias -> { roomSummaries: List<RoomSummary> -> timeout: Duration
val found = roomSummaries.find { it.aliases.contains(roomIdOrAlias.roomAlias) } ): RoomSummary {
found != null && found.currentUserMembership == CurrentUserMembership.JOINED
}
is RoomIdOrAlias.Id -> { roomSummaries: List<RoomSummary> ->
val found = roomSummaries.find { it.roomId == roomIdOrAlias.roomId }
found != null && found.currentUserMembership == CurrentUserMembership.JOINED
}
}
return withTimeout(timeout) { return withTimeout(timeout) {
roomListService.allRooms.summaries getRoomSummaryFlow(roomIdOrAlias)
.filter(predicate) .mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() }
.first() .filter { roomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.JOINED }
.first() .first()
// Ensure that the room is ready // Ensure that the room is ready
.also { client.awaitRoomRemoteEcho(it.roomId.value) } .also { client.awaitRoomRemoteEcho(it.roomId.value) }
@ -568,20 +560,21 @@ class RustMatrixClient(
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
override fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>> { override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<RoomSummary>> {
return flow { val predicate: (RoomSummary) -> Boolean = when (roomIdOrAlias) {
var room = getRoom(roomId) is RoomIdOrAlias.Alias -> { roomSummary ->
if (room == null) { roomSummary.info.aliases.contains(roomIdOrAlias.roomAlias)
emit(Optional.empty())
awaitJoinedRoom(roomId.toRoomIdOrAlias(), INFINITE)
room = getRoom(roomId)
} }
room?.use { is RoomIdOrAlias.Id -> { roomSummary ->
room.roomInfoFlow roomSummary.roomId == roomIdOrAlias.roomId
.map { roomInfo -> Optional.of(roomInfo) }
.collect(this)
} }
}.distinctUntilChanged() }
return roomListService.allRooms.summaries
.map { roomSummaries ->
val roomSummary = roomSummaries.firstOrNull(predicate)
Optional.ofNullable(roomSummary)
}
.distinctUntilChanged()
} }
override suspend fun setAllSendQueuesEnabled(enabled: Boolean) = withContext(sessionDispatcher) { override suspend fun setAllSendQueuesEnabled(enabled: Boolean) = withContext(sessionDispatcher) {

View file

@ -53,6 +53,10 @@ class MatrixRoomInfoMapper {
activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(), activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(),
heroes = it.elementHeroes().toImmutableList(), heroes = it.elementHeroes().toImmutableList(),
pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(), pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(),
isMarkedUnread = it.isMarkedUnread,
numUnreadMessages = it.numUnreadMessages.toLong(),
numUnreadMentions = it.numUnreadMentions.toLong(),
numUnreadNotifications = it.numUnreadNotifications.toLong(),
) )
} }
} }

View file

@ -31,7 +31,7 @@ internal class RoomListFactory(
private val innerRoomListService: RoomListService, private val innerRoomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
) { ) {
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory() private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory()
/** /**
* Creates a room list that can be used to load more rooms and filter them dynamically. * Creates a room list that can be used to load more rooms and filter them dynamically.

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.roomlist package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
@ -17,19 +18,19 @@ val RoomListFilter.predicate
is RoomListFilter.Any -> { _: RoomSummary -> true } is RoomListFilter.Any -> { _: RoomSummary -> true }
RoomListFilter.None -> { _: RoomSummary -> false } RoomListFilter.None -> { _: RoomSummary -> false }
RoomListFilter.Category.Group -> { roomSummary: RoomSummary -> RoomListFilter.Category.Group -> { roomSummary: RoomSummary ->
!roomSummary.isDm && !roomSummary.isInvited() !roomSummary.info.isDm && !roomSummary.isInvited()
} }
RoomListFilter.Category.People -> { roomSummary: RoomSummary -> RoomListFilter.Category.People -> { roomSummary: RoomSummary ->
roomSummary.isDm && !roomSummary.isInvited() roomSummary.info.isDm && !roomSummary.isInvited()
} }
RoomListFilter.Favorite -> { roomSummary: RoomSummary -> RoomListFilter.Favorite -> { roomSummary: RoomSummary ->
roomSummary.isFavorite && !roomSummary.isInvited() roomSummary.info.isFavorite && !roomSummary.isInvited()
} }
RoomListFilter.Unread -> { roomSummary: RoomSummary -> RoomListFilter.Unread -> { roomSummary: RoomSummary ->
!roomSummary.isInvited() && (roomSummary.numUnreadNotifications > 0 || roomSummary.isMarkedUnread) !roomSummary.isInvited() && (roomSummary.info.numUnreadNotifications > 0 || roomSummary.info.isMarkedUnread)
} }
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary -> is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
roomSummary.name.orEmpty().contains(pattern, ignoreCase = true) roomSummary.info.name.orEmpty().contains(pattern, ignoreCase = true)
} }
RoomListFilter.Invite -> { roomSummary: RoomSummary -> RoomListFilter.Invite -> { roomSummary: RoomSummary ->
roomSummary.isInvited() roomSummary.isInvited()
@ -50,4 +51,4 @@ fun List<RoomSummary>.filter(filter: RoomListFilter): List<RoomSummary> {
} }
} }
private fun RoomSummary.isInvited() = currentUserMembership == CurrentUserMembership.INVITED private fun RoomSummary.isInvited() = info.currentUserMembership == CurrentUserMembership.INVITED

View file

@ -1,51 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
import io.element.android.libraries.matrix.impl.room.elementHeroes
import io.element.android.libraries.matrix.impl.room.map
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
class RoomSummaryDetailsFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo()
val latestRoomMessage = roomListItem.latestEvent().use { event ->
roomMessageFactory.create(event)
}
return RoomSummary(
roomId = RoomId(roomInfo.id),
name = roomInfo.displayName,
canonicalAlias = roomInfo.canonicalAlias?.let(::RoomAlias),
alternativeAliases = roomInfo.alternativeAliases.map(::RoomAlias),
isDirect = roomInfo.isDirect,
avatarUrl = roomInfo.avatarUrl,
numUnreadMentions = roomInfo.numUnreadMentions.toInt(),
numUnreadMessages = roomInfo.numUnreadMessages.toInt(),
numUnreadNotifications = roomInfo.numUnreadNotifications.toInt(),
isMarkedUnread = roomInfo.isMarkedUnread,
lastMessage = latestRoomMessage,
inviter = roomInfo.inviter?.let(RoomMemberMapper::map),
userDefinedNotificationMode = roomInfo.cachedUserDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),
hasRoomCall = roomInfo.hasRoomCall,
isDm = isDm(isDirect = roomInfo.isDirect, activeMembersCount = roomInfo.activeMembersCount.toInt()),
isFavorite = roomInfo.isFavourite,
currentUserMembership = roomInfo.membership.map(),
heroes = roomInfo.elementHeroes(),
)
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
class RoomSummaryFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
private val roomInfoMapper: MatrixRoomInfoMapper = MatrixRoomInfoMapper(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo().let(roomInfoMapper::map)
val latestRoomMessage = roomListItem.latestEvent().use { event ->
roomMessageFactory.create(event)
}
return RoomSummary(
info = roomInfo,
lastMessage = latestRoomMessage,
)
}
}

View file

@ -23,7 +23,7 @@ class RoomSummaryListProcessor(
private val roomSummaries: MutableSharedFlow<List<RoomSummary>>, private val roomSummaries: MutableSharedFlow<List<RoomSummary>>,
private val roomListService: RoomListServiceInterface, private val roomListService: RoomListServiceInterface,
private val coroutineContext: CoroutineContext, private val coroutineContext: CoroutineContext,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory(),
) { ) {
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>() private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
private val mutex = Mutex() private val mutex = Mutex()

View file

@ -105,6 +105,10 @@ class MatrixRoomInfoMapperTest {
).toImmutableList(), ).toImmutableList(),
pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(), pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(),
creator = A_USER_ID, creator = A_USER_ID,
isMarkedUnread = false,
numUnreadMessages = 12L,
numUnreadNotifications = 13L,
numUnreadMentions = 14L,
) )
) )
} }
@ -174,6 +178,10 @@ class MatrixRoomInfoMapperTest {
heroes = emptyList<MatrixUser>().toImmutableList(), heroes = emptyList<MatrixUser>().toImmutableList(),
pinnedEventIds = emptyList<EventId>().toPersistentList(), pinnedEventIds = emptyList<EventId>().toPersistentList(),
creator = null, creator = null,
isMarkedUnread = true,
numUnreadMessages = 12L,
numUnreadNotifications = 13L,
numUnreadMentions = 14L,
) )
) )
} }

View file

@ -19,7 +19,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
@ -29,7 +29,7 @@ import org.junit.Test
class DefaultJoinRoomTest { class DefaultJoinRoomTest {
@Test @Test
fun `when using roomId and there is no server names, the classic join room API is used`() = runTest { fun `when using roomId and there is no server names, the classic join room API is used`() = runTest {
val roomSummary = aRoomSummaryFilled() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom()
@ -64,7 +64,7 @@ class DefaultJoinRoomTest {
@Test @Test
fun `when using roomId and server names are available, joinRoomByIdOrAlias API is used`() = runTest { fun `when using roomId and server names are available, joinRoomByIdOrAlias API is used`() = runTest {
val roomSummary = aRoomSummaryFilled() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom()
@ -100,7 +100,7 @@ class DefaultJoinRoomTest {
@Test @Test
fun `when using roomAlias, joinRoomByIdOrAlias API is used`() = runTest { fun `when using roomAlias, joinRoomByIdOrAlias API is used`() = runTest {
val roomSummary = aRoomSummaryFilled() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom()

View file

@ -16,10 +16,11 @@ import org.junit.Test
class RoomListFilterTest { class RoomListFilterTest {
private val regularRoom = aRoomSummary( private val regularRoom = aRoomSummary(
isDm = false isDirect = false,
) )
private val dmRoom = aRoomSummary( private val dmRoom = aRoomSummary(
isDm = true isDirect = true,
activeMembersCount = 2
) )
private val favoriteRoom = aRoomSummary( private val favoriteRoom = aRoomSummary(
isFavorite = true isFavorite = true

View file

@ -15,7 +15,6 @@ 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_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID_3 import io.element.android.libraries.matrix.test.A_ROOM_ID_3
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -40,7 +39,7 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `PushBack adds a new entry at the end of the list`() = runTest { fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2)))) processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2))))
@ -50,7 +49,7 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `PushFront inserts a new entry at the start of the list`() = runTest { fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2)))) processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2))))
@ -60,7 +59,7 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Set replaces an entry at some index`() = runTest { fun `Set replaces an entry at some index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -72,7 +71,7 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Insert inserts a new entry at the provided index`() = runTest { fun `Insert inserts a new entry at the provided index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -84,7 +83,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Remove removes an entry at some index`() = runTest { fun `Remove removes an entry at some index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -96,7 +98,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `PopBack removes an entry at the end of the list`() = runTest { fun `PopBack removes an entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -108,7 +113,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `PopFront removes an entry at the start of the list`() = runTest { fun `PopFront removes an entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -120,7 +128,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Clear removes all the entries`() = runTest { fun `Clear removes all the entries`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear)) processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
@ -130,7 +141,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Truncate removes all entries after the provided length`() = runTest { fun `Truncate removes all entries after the provided length`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -142,7 +156,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Reset removes all entries and add the provided ones`() = runTest { fun `Reset removes all entries and add the provided ones`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -156,6 +173,6 @@ class RoomSummaryListProcessorTest {
summaries, summaries,
FakeRustRoomListService(), FakeRustRoomListService(),
coroutineContext = StandardTestDispatcher(testScheduler), coroutineContext = StandardTestDispatcher(testScheduler),
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(), roomSummaryDetailsFactory = RoomSummaryFactory(),
) )
} }

View file

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.InvitedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@ -118,8 +117,8 @@ class FakeMatrixClient(
var knockRoomLambda: (RoomId) -> Result<Unit> = { var knockRoomLambda: (RoomId) -> Result<Unit> = {
Result.success(Unit) Result.success(Unit)
} }
var getRoomInfoFlowLambda = { _: RoomId -> var getRoomSummaryFlowLambda = { _: RoomIdOrAlias ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty()) flowOf<Optional<RoomSummary>>(Optional.empty())
} }
var logoutLambda: (Boolean, Boolean) -> String? = { _, _ -> var logoutLambda: (Boolean, Boolean) -> String? = { _, _ ->
null null
@ -316,7 +315,7 @@ class FakeMatrixClient(
return Result.success(visitedRoomsId) return Result.success(visitedRoomsId)
} }
override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomSummaryFlowLambda(roomIdOrAlias)
var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean -> var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean ->
// no-op // no-op

View file

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
@ -32,7 +31,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
@ -40,21 +38,15 @@ import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerL
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
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_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.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -527,62 +519,6 @@ class FakeMatrixRoom(
} }
} }
fun aRoomInfo(
id: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
rawName: String? = name,
topic: String? = "A topic",
avatarUrl: String? = AN_AVATAR_URL,
isDirect: Boolean = false,
isPublic: Boolean = true,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
isFavorite: Boolean = false,
canonicalAlias: RoomAlias? = null,
alternativeAliases: List<RoomAlias> = emptyList(),
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
inviter: RoomMember? = null,
activeMembersCount: Long = 1,
invitedMembersCount: Long = 0,
joinedMembersCount: Long = 1,
highlightCount: Long = 0,
notificationCount: Long = 0,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<UserId> = emptyList(),
heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(),
roomCreator: UserId? = null,
) = MatrixRoomInfo(
id = id,
name = name,
rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavorite = isFavorite,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases.toImmutableList(),
currentUserMembership = currentUserMembership,
inviter = inviter,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
highlightCount = highlightCount,
notificationCount = notificationCount,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
userPowerLevels = userPowerLevels,
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
heroes = heroes.toImmutableList(),
pinnedEventIds = pinnedEventIds.toImmutableList(),
creator = roomCreator,
)
fun defaultRoomPowerLevels() = MatrixRoomPowerLevels( fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(
ban = 50, ban = 50,
invite = 0, invite = 0,

View file

@ -0,0 +1,90 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.MatrixUser
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_NAME
import io.element.android.libraries.matrix.test.A_ROOM_RAW_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
fun aRoomInfo(
id: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
rawName: String? = A_ROOM_RAW_NAME,
topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = AN_AVATAR_URL,
isDirect: Boolean = false,
isPublic: Boolean = true,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
isFavorite: Boolean = false,
canonicalAlias: RoomAlias? = null,
alternativeAliases: List<RoomAlias> = emptyList(),
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
inviter: RoomMember? = null,
activeMembersCount: Long = 1,
invitedMembersCount: Long = 0,
joinedMembersCount: Long = 1,
highlightCount: Long = 0,
notificationCount: Long = 0,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<UserId> = emptyList(),
heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(),
roomCreator: UserId? = null,
isMarkedUnread: Boolean = false,
numUnreadMessages: Long = 0,
numUnreadNotifications: Long = 0,
numUnreadMentions: Long = 0,
) = MatrixRoomInfo(
id = id,
name = name,
rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavorite = isFavorite,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases.toImmutableList(),
currentUserMembership = currentUserMembership,
inviter = inviter,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
highlightCount = highlightCount,
notificationCount = notificationCount,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
userPowerLevels = userPowerLevels,
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
heroes = heroes.toImmutableList(),
pinnedEventIds = pinnedEventIds.toImmutableList(),
creator = roomCreator,
isMarkedUnread = isMarkedUnread,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
numUnreadMentions = numUnreadMentions,
)

View file

@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.room.message.RoomMessage
@ -21,69 +22,88 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
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_ROOM_RAW_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toPersistentList
fun aRoomSummaryFilled( fun aRoomSummary(
roomId: RoomId = A_ROOM_ID, info: MatrixRoomInfo = aRoomInfo(),
name: String = A_ROOM_NAME,
isDirect: Boolean = false,
avatarUrl: String? = null,
lastMessage: RoomMessage? = aRoomMessage(), lastMessage: RoomMessage? = aRoomMessage(),
numUnreadMentions: Int = 0, ) = RoomSummary(
numUnreadMessages: Int = 0, info = info,
notificationMode: RoomNotificationMode? = null,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
) = aRoomSummary(
roomId = roomId,
name = name,
isDirect = isDirect,
avatarUrl = avatarUrl,
lastMessage = lastMessage, lastMessage = lastMessage,
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
notificationMode = notificationMode,
currentUserMembership = currentUserMembership,
) )
fun aRoomSummary( fun aRoomSummary(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME, name: String? = A_ROOM_NAME,
isDirect: Boolean = false, rawName: String? = A_ROOM_RAW_NAME,
topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = null, avatarUrl: String? = null,
lastMessage: RoomMessage? = aRoomMessage(), isDirect: Boolean = false,
numUnreadMentions: Int = 0, isPublic: Boolean = true,
numUnreadMessages: Int = 0, isSpace: Boolean = false,
numUnreadNotifications: Int = 0, isTombstoned: Boolean = false,
isMarkedUnread: Boolean = false, isFavorite: Boolean = false,
notificationMode: RoomNotificationMode? = null,
inviter: RoomMember? = null,
canonicalAlias: RoomAlias? = null, canonicalAlias: RoomAlias? = null,
alternativeAliases: List<RoomAlias> = emptyList(), alternativeAliases: List<RoomAlias> = emptyList(),
hasRoomCall: Boolean = false,
isDm: Boolean = false,
isFavorite: Boolean = false,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
inviter: RoomMember? = null,
activeMembersCount: Long = 1,
invitedMembersCount: Long = 0,
joinedMembersCount: Long = 1,
highlightCount: Long = 0,
notificationCount: Long = 0,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<UserId> = emptyList(),
heroes: List<MatrixUser> = emptyList(), heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(),
roomCreator: UserId? = null,
isMarkedUnread: Boolean = false,
numUnreadMessages: Long = 0,
numUnreadNotifications: Long = 0,
numUnreadMentions: Long = 0,
lastMessage: RoomMessage? = aRoomMessage(),
) = RoomSummary( ) = RoomSummary(
roomId = roomId, info = MatrixRoomInfo(
name = name, id = roomId,
isDirect = isDirect, name = name,
avatarUrl = avatarUrl, rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavorite = isFavorite,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases.toPersistentList(),
currentUserMembership = currentUserMembership,
inviter = inviter,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
userPowerLevels = userPowerLevels,
highlightCount = highlightCount,
notificationCount = notificationCount,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
activeRoomCallParticipants = activeRoomCallParticipants.toPersistentList(),
heroes = heroes.toPersistentList(),
pinnedEventIds = pinnedEventIds.toPersistentList(),
creator = roomCreator,
isMarkedUnread = isMarkedUnread,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
numUnreadMentions = numUnreadMentions,
),
lastMessage = lastMessage, lastMessage = lastMessage,
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
userDefinedNotificationMode = notificationMode,
inviter = inviter,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases,
hasRoomCall = hasRoomCall,
isDm = isDm,
isFavorite = isFavorite,
currentUserMembership = currentUserMembership,
heroes = heroes,
) )
fun aRoomMessage( fun aRoomMessage(

View file

@ -1,66 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.user.MatrixUser
open class RoomSummaryDetailsProvider : PreviewParameterProvider<RoomSummary> {
override val values: Sequence<RoomSummary>
get() = sequenceOf(
aRoomSummaryDetails(),
aRoomSummaryDetails(name = null),
)
}
fun aRoomSummaryDetails(
roomId: RoomId = RoomId("!room:domain"),
name: String? = "roomName",
canonicalAlias: RoomAlias? = null,
alternativeAliases: List<RoomAlias> = emptyList(),
isDirect: Boolean = true,
avatarUrl: String? = null,
lastMessage: RoomMessage? = null,
inviter: RoomMember? = null,
notificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
isDm: Boolean = false,
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
numUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
isFavorite: Boolean = false,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
heroes: List<MatrixUser> = emptyList(),
) = RoomSummary(
roomId = roomId,
name = name,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases,
isDirect = isDirect,
avatarUrl = avatarUrl,
lastMessage = lastMessage,
inviter = inviter,
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
isDm = isDm,
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
isFavorite = isFavorite,
currentUserMembership = currentUserMembership,
heroes = heroes,
)

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
class SelectRoomInfoProvider : PreviewParameterProvider<SelectRoomInfo> {
override val values: Sequence<SelectRoomInfo>
get() = sequenceOf(
aSelectRoomInfo(roomId = RoomId("!room1:domain")),
aSelectRoomInfo(roomId = RoomId("!room2:domain"), name = "Room with a name"),
aSelectRoomInfo(roomId = RoomId("!room3:domain"), name = "Room with a name and alias", canonicalAlias = RoomAlias("#alias:domain")),
)
}
fun aSelectRoomInfo(
roomId: RoomId,
name: String? = null,
canonicalAlias: RoomAlias? = null,
avatarUrl: String? = null,
heroes: ImmutableList<MatrixUser> = persistentListOf(),
) = SelectRoomInfo(
roomId = roomId,
name = name,
canonicalAlias = canonicalAlias,
avatarUrl = avatarUrl,
heroes = heroes,
)

View file

@ -34,15 +34,15 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun SelectedRoom( fun SelectedRoom(
roomSummary: RoomSummary, roomInfo: SelectRoomInfo,
onRemoveRoom: (RoomSummary) -> Unit, onRemoveRoom: (SelectRoomInfo) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box( Box(
@ -53,14 +53,12 @@ fun SelectedRoom(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
CompositeAvatar( CompositeAvatar(
avatarData = roomSummary.getAvatarData(size = AvatarSize.SelectedRoom), avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom),
heroes = roomSummary.heroes.map { user -> heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(),
user.getAvatarData(size = AvatarSize.SelectedRoom)
}.toImmutableList()
) )
Text( Text(
// If name is null, we do not have space to render "No room name", so just use `#` here. // If name is null, we do not have space to render "No room name", so just use `#` here.
text = roomSummary.name ?: "#", text = roomInfo.name ?: "#",
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
@ -69,14 +67,14 @@ fun SelectedRoom(
Surface( Surface(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.size(20.dp) .size(20.dp)
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.clickable( .clickable(
indication = ripple(), indication = ripple(),
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
onClick = { onRemoveRoom(roomSummary) } onClick = { onRemoveRoom(roomInfo) }
), ),
) { ) {
Icon( Icon(
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Close(),
@ -91,10 +89,10 @@ fun SelectedRoom(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun SelectedRoomPreview( internal fun SelectedRoomPreview(
@PreviewParameter(RoomSummaryDetailsProvider::class) roomSummary: RoomSummary @PreviewParameter(SelectRoomInfoProvider::class) roomInfo: SelectRoomInfo
) = ElementPreview { ) = ElementPreview {
SelectedRoom( SelectedRoom(
roomSummary = roomSummary, roomInfo = roomInfo,
onRemoveRoom = {}, onRemoveRoom = {},
) )
} }

View file

@ -9,10 +9,10 @@ package io.element.android.libraries.matrix.ui.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
fun RoomSummary.getAvatarData(size: AvatarSize) = AvatarData( fun MatrixRoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
id = roomId.value, id = id.value,
name = name, name = name,
url = avatarUrl, url = avatarUrl,
size = size, size = size,

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class SelectRoomInfo(
val roomId: RoomId,
val name: String?,
val canonicalAlias: RoomAlias?,
val avatarUrl: String?,
val heroes: ImmutableList<MatrixUser>,
) {
fun getAvatarData(size: AvatarSize) = AvatarData(
id = roomId.value,
name = name,
url = avatarUrl,
size = size,
)
}
fun RoomSummary.toSelectRoomInfo() = SelectRoomInfo(
roomId = roomId,
name = info.name,
avatarUrl = info.avatarUrl,
heroes = info.heroes,
canonicalAlias = info.canonicalAlias,
)

View file

@ -7,10 +7,10 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
sealed interface RoomSelectEvents { sealed interface RoomSelectEvents {
data class SetSelectedRoom(val room: RoomSummary) : RoomSelectEvents data class SetSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvents
// TODO remove to restore multi-selection // TODO remove to restore multi-selection
data object RemoveSelectedRoom : RoomSelectEvents data object RemoveSelectedRoom : RoomSelectEvents

View file

@ -20,7 +20,7 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -36,7 +36,7 @@ class RoomSelectPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): RoomSelectState { override fun present(): RoomSelectState {
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummary>()) } var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
var searchQuery by remember { mutableStateOf("") } var searchQuery by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) } var isSearchActive by remember { mutableStateOf(false) }
@ -48,7 +48,7 @@ class RoomSelectPresenter @AssistedInject constructor(
dataSource.setSearchQuery(searchQuery) dataSource.setSearchQuery(searchQuery)
} }
val roomSummaryDetailsList by dataSource.roomSummaries.collectAsState(initial = persistentListOf()) val roomSummaryDetailsList by dataSource.roomInfoList.collectAsState(initial = persistentListOf())
val searchResults by remember { val searchResults by remember {
derivedStateOf { derivedStateOf {

View file

@ -12,8 +12,9 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -38,11 +39,12 @@ class RoomSelectSearchDataSource @Inject constructor(
source = RoomList.Source.All, source = RoomList.Source.All,
) )
val roomSummaries: Flow<PersistentList<RoomSummary>> = roomList.filteredSummaries val roomInfoList: Flow<PersistentList<SelectRoomInfo>> = roomList.filteredSummaries
.map { roomSummaries -> .map { roomSummaries ->
roomSummaries roomSummaries
.filter { it.currentUserMembership == CurrentUserMembership.JOINED } .filter { it.info.currentUserMembership == CurrentUserMembership.JOINED }
.distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received .distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received
.map { roomSummary -> roomSummary.toSelectRoomInfo() }
.toPersistentList() .toPersistentList()
} }
.flowOn(coroutineDispatchers.computation) .flowOn(coroutineDispatchers.computation)

View file

@ -8,15 +8,15 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class RoomSelectState( data class RoomSelectState(
val mode: RoomSelectMode, val mode: RoomSelectMode,
val resultState: SearchBarResultState<ImmutableList<RoomSummary>>, val resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
val query: String, val query: String,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val selectedRooms: ImmutableList<RoomSummary>, val selectedRooms: ImmutableList<SelectRoomInfo>,
val eventSink: (RoomSelectEvents) -> Unit val eventSink: (RoomSelectEvents) -> Unit
) )

View file

@ -11,8 +11,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -32,7 +32,7 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
resultState = SearchBarResultState.Results(aRoomSelectRoomList()), resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
query = "Test", query = "Test",
isSearchActive = true, isSearchActive = true,
selectedRooms = persistentListOf(aRoomSummaryDetails(roomId = RoomId("!room2:domain"))) selectedRooms = aRoomSelectRoomList().subList(0, 1),
), ),
aRoomSelectState( aRoomSelectState(
mode = RoomSelectMode.Share, mode = RoomSelectMode.Share,
@ -43,10 +43,10 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
private fun aRoomSelectState( private fun aRoomSelectState(
mode: RoomSelectMode = RoomSelectMode.Forward, mode: RoomSelectMode = RoomSelectMode.Forward,
resultState: SearchBarResultState<ImmutableList<RoomSummary>> = SearchBarResultState.Initial(), resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
query: String = "", query: String = "",
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
selectedRooms: ImmutableList<RoomSummary> = persistentListOf(), selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
) = RoomSelectState( ) = RoomSelectState(
mode = mode, mode = mode,
resultState = resultState, resultState = resultState,
@ -57,14 +57,16 @@ private fun aRoomSelectState(
) )
private fun aRoomSelectRoomList() = persistentListOf( private fun aRoomSelectRoomList() = persistentListOf(
aRoomSummaryDetails(), aSelectRoomInfo(
aRoomSummaryDetails( roomId = RoomId("!room1:domain"),
name = "Room with name",
),
aSelectRoomInfo(
roomId = RoomId("!room2:domain"), roomId = RoomId("!room2:domain"),
name = "Room with alias", name = "Room with alias",
canonicalAlias = RoomAlias("#alias:example.org"), canonicalAlias = RoomAlias("#alias:example.org"),
), ),
aRoomSummaryDetails( aSelectRoomInfo(
roomId = RoomId("!room3:domain"), roomId = RoomId("!room3:domain"),
name = null,
), ),
) )

View file

@ -47,8 +47,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.components.SelectedRoom import io.element.android.libraries.matrix.ui.components.SelectedRoom
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -65,13 +65,13 @@ fun RoomSelectView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun onRoomRemoved(roomSummary: RoomSummary) { fun onRoomRemoved(roomInfo: SelectRoomInfo) {
// TODO toggle selection when multi-selection is enabled // TODO toggle selection when multi-selection is enabled
state.eventSink(RoomSelectEvents.RemoveSelectedRoom) state.eventSink(RoomSelectEvents.RemoveSelectedRoom)
} }
@Composable @Composable
fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<RoomSummary>) { fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<SelectRoomInfo>) {
if (isForwarding) return if (isForwarding) return
SelectedRooms( SelectedRooms(
selectedRooms = selectedRooms, selectedRooms = selectedRooms,
@ -185,8 +185,8 @@ fun RoomSelectView(
@Composable @Composable
private fun SelectedRooms( private fun SelectedRooms(
selectedRooms: ImmutableList<RoomSummary>, selectedRooms: ImmutableList<SelectRoomInfo>,
onRemoveRoom: (RoomSummary) -> Unit, onRemoveRoom: (SelectRoomInfo) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyRow( LazyRow(
@ -194,29 +194,29 @@ private fun SelectedRooms(
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(32.dp) horizontalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
items(selectedRooms, key = { it.roomId.value }) { roomSummary -> items(selectedRooms, key = { it.roomId.value }) { selectRoomInfo ->
SelectedRoom(roomSummary = roomSummary, onRemoveRoom = onRemoveRoom) SelectedRoom(roomInfo = selectRoomInfo, onRemoveRoom = onRemoveRoom)
} }
} }
} }
@Composable @Composable
private fun RoomSummaryView( private fun RoomSummaryView(
summary: RoomSummary, roomInfo: SelectRoomInfo,
isSelected: Boolean, isSelected: Boolean,
onSelection: (RoomSummary) -> Unit, onSelection: (SelectRoomInfo) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.clickable { onSelection(summary) } .clickable { onSelection(roomInfo) }
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 4.dp) .padding(start = 16.dp, end = 4.dp)
.heightIn(56.dp), .heightIn(56.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CompositeAvatar( CompositeAvatar(
avatarData = summary.getAvatarData(size = AvatarSize.RoomSelectRoomListItem), avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
heroes = summary.heroes.map { user -> heroes = roomInfo.heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem) user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
}.toPersistentList() }.toPersistentList()
) )
@ -228,14 +228,14 @@ private fun RoomSummaryView(
// Name // Name
Text( Text(
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
text = summary.name ?: stringResource(id = CommonStrings.common_no_room_name), text = roomInfo.name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { summary.name == null }, fontStyle = FontStyle.Italic.takeIf { roomInfo.name == null },
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
// Alias // Alias
summary.canonicalAlias?.let { alias -> roomInfo.canonicalAlias?.let { alias ->
Text( Text(
text = alias.value, text = alias.value,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
@ -245,7 +245,7 @@ private fun RoomSummaryView(
) )
} }
} }
RadioButton(selected = isSelected, onClick = { onSelection(summary) }) RadioButton(selected = isSelected, onClick = { onSelection(roomInfo) })
} }
} }

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
@ -58,8 +59,9 @@ class RoomSelectPresenterTest {
@Test @Test
fun `present - update query`() = runTest { fun `present - update query`() = runTest {
val roomSummary = aRoomSummary()
val roomListService = FakeRoomListService().apply { val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(aRoomSummary())) postAllRooms(listOf(roomSummary))
} }
val presenter = createRoomSelectPresenter( val presenter = createRoomSelectPresenter(
roomListService = roomListService roomListService = roomListService
@ -68,19 +70,10 @@ class RoomSelectPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
val expectedRoomSummary = aRoomSummary() val expectedRoomInfo = roomSummary.toSelectRoomInfo()
// Do not compare the lambda because they will be different. So copy the lambda from expectedRoomSummary to result // Do not compare the lambda because they will be different. So copy the lambda from expectedRoomSummary to result
val result = (awaitItem().resultState as SearchBarResultState.Results).results.map { roomSummary -> val result = (awaitItem().resultState as SearchBarResultState.Results).results
roomSummary.copy( assertThat(result).isEqualTo(listOf(expectedRoomInfo))
lastMessage = roomSummary.lastMessage!!.copy(
event = roomSummary.lastMessage!!.event.copy(
debugInfoProvider = expectedRoomSummary.lastMessage!!.event.debugInfoProvider,
messageShieldProvider = expectedRoomSummary.lastMessage!!.event.messageShieldProvider,
)
),
)
}
assertThat(result).isEqualTo(listOf(expectedRoomSummary))
initialState.eventSink(RoomSelectEvents.ToggleSearchActive) initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
skipItems(1) skipItems(1)
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained")) initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
@ -99,8 +92,9 @@ class RoomSelectPresenterTest {
@Test @Test
fun `present - select and remove a room`() = runTest { fun `present - select and remove a room`() = runTest {
val roomSummary = aRoomSummary()
val roomListService = FakeRoomListService().apply { val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(aRoomSummary())) postAllRooms(listOf(roomSummary))
} }
val presenter = createRoomSelectPresenter( val presenter = createRoomSelectPresenter(
roomListService = roomListService, roomListService = roomListService,
@ -109,9 +103,9 @@ class RoomSelectPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
val summary = aRoomSummary() val roomInfo = roomSummary.toSelectRoomInfo()
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary)) initialState.eventSink(RoomSelectEvents.SetSelectedRoom(roomInfo))
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary)) assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(roomInfo))
initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom) initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom)
assertThat(awaitItem().selectedRooms).isEmpty() assertThat(awaitItem().selectedRooms).isEmpty()
cancel() cancel()

View file

@ -8,13 +8,27 @@
package io.element.android.libraries.textcomposer.mentions package io.element.android.libraries.textcomposer.mentions
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
@Immutable @Immutable
sealed interface ResolvedSuggestion { sealed interface ResolvedSuggestion {
data object AtRoom : ResolvedSuggestion data object AtRoom : ResolvedSuggestion
data class Member(val roomMember: RoomMember) : ResolvedSuggestion data class Member(val roomMember: RoomMember) : ResolvedSuggestion
data class Alias(val roomAlias: RoomAlias, val roomSummary: RoomSummary) : ResolvedSuggestion data class Alias(
val roomAlias: RoomAlias,
val roomId: RoomId,
val roomName: String?,
val roomAvatarUrl: String?,
) : ResolvedSuggestion {
fun getAvatarData(size: AvatarSize) = AvatarData(
id = roomId.value,
name = roomName,
url = roomAvatarUrl,
size = size,
)
}
} }

View file

@ -16,10 +16,10 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.textcomposer.mentions.MentionSpan import io.element.android.libraries.textcomposer.mentions.MentionSpan
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
@ -34,7 +34,7 @@ class MarkdownTextEditorStateTest {
@Test @Test
fun `insertMention - room alias - getMentions return empty list`() { fun `insertMention - room alias - getMentions return empty list`() {
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true) val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true)
val suggestion = ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary(canonicalAlias = A_ROOM_ALIAS)) val suggestion = aRoomAliasSuggestion()
val permalinkBuilder = FakePermalinkBuilder() val permalinkBuilder = FakePermalinkBuilder()
val mentionSpanProvider = aMentionSpanProvider() val mentionSpanProvider = aMentionSpanProvider()
state.insertSuggestion(suggestion, mentionSpanProvider, permalinkBuilder) state.insertSuggestion(suggestion, mentionSpanProvider, permalinkBuilder)
@ -46,7 +46,7 @@ class MarkdownTextEditorStateTest {
val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply { val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply {
currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Room, text = "") currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Room, text = "")
} }
val suggestion = ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary(canonicalAlias = A_ROOM_ALIAS)) val suggestion = aRoomAliasSuggestion()
val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) })
val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.failure(IllegalStateException("Failed")) }) val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.failure(IllegalStateException("Failed")) })
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
@ -58,7 +58,7 @@ class MarkdownTextEditorStateTest {
val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply { val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply {
currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Room, text = "") currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Room, text = "")
} }
val suggestion = ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary(canonicalAlias = A_ROOM_ALIAS)) val suggestion = aRoomAliasSuggestion()
val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) })
val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.success("https://matrix.to/#/${A_ROOM_ALIAS.value}") }) val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.success("https://matrix.to/#/${A_ROOM_ALIAS.value}") })
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
@ -202,4 +202,13 @@ class MarkdownTextEditorStateTest {
} }
} }
} }
private fun aRoomAliasSuggestion(): ResolvedSuggestion.Alias {
return ResolvedSuggestion.Alias(
roomAlias = A_ROOM_ALIAS,
roomId = A_ROOM_ID,
roomName = null,
roomAvatarUrl = null
)
}
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7d984f18fc4abf9fe56afc74f094aaacfb6926642b37d2d5fc11568707e3f331 oid sha256:19f6f6dcdf49e9ffd2fbb0ea81c3fe0374faf79cecccc812ea1e1d8c05578671
size 22296 size 22344

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:853541fe1bf56895ed286791d68e13d6f07bd8a5a1fbdaafe5354f10a916f5c0 oid sha256:12266ee1fc3477aec0f47e731294ccd2f93ecf4a3a1fada6cf4cb8ba6f7430ac
size 22620 size 22661

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b6dbc970789b59a9bc7db1257a6c60b9b33b7eb7747a30482802659303582323 oid sha256:cbae54843c22fdcb012d926191bc01e638ba38dc55eb4b24c9e3ad9e9a9d103a
size 7340 size 6807

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:42e59e3add65ddc468cb23d9b2bfd460d39c37b72a09deb12298a0bb28d362dc oid sha256:764de9f67cabfe22be6ad3021a8418d3677f709fce95c019cd66820e66359980
size 6752 size 7804

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ecdfbc386e865be164e34ef963330be1c8548e8d3a505f64321d6496a072896b
size 7670

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bcbb1951463a29e167d3ae09be263461bb449ca8677229fe00f18b6a60c8c31d oid sha256:428c291e304d9f2c25658569a8ecf324eafccf4b63b31897727dda8151289d05
size 7535 size 7040

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3118842f8b80ab4ffdcfbd8124cebaf536d131700d884ec06aa6b19ce22f742c oid sha256:345cb5d50b201e957be4aaed8b2ac785cd4dfc795924e28ba45015b62bb0d1d7
size 6954 size 7707

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0ba819713f7d3bdc667481726af1e60366a30c62148893f2eef5e503c5e1df0
size 7827

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f7e19544c7a255dc1c52f69c1387966a806ffca809f6ad071b9bc501c86a9577 oid sha256:0db3f0cdb53eb761bf3742ce94f925472a7e1b48b306d2cd81baa2c42bee6a14
size 30327 size 31142

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a417fb437c823b44a62a4e59be87f44425de32ad347115d800f74d4d2db98ecc oid sha256:d1a17ed95a976a813b541eb373e2eb1f50388f6a994da7b65a586d31be8e0d23
size 28361 size 29199

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:207aa1698d4b851d6c3b97cd99ade7499abb1882c5d8a6450c83de596430a448 oid sha256:1d5390b71350b1029a153c770f1e525c6346052043583fad2b3c4aadfb694b16
size 32767 size 33441

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d8278dba6831714c5f27c8389e4dc048c6d20404bd679e888891c2053baac351 oid sha256:5aa120484502f3ed14e33a7460b19bd090ac421bbb383e75127bbdf3f54f5ca3
size 27995 size 28818

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d89466ad2d6274b2def2b1354862490445bedc0d02e73aec8e9bd1fa77d7133c oid sha256:646bf7110d66643fda586c2ecd54b78f09111d0e74a531a7c1fa08e968b1abc3
size 29405 size 30217

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8bd095f282b4c40bf7f74a3487bcf3bcbea0b2c0f352f9e02400ee6312d75ed6 oid sha256:594841dd922deceeee60da8bdd7228f9c685f2aa700a2ccbe0dd03bd4b9e4b98
size 27953 size 28739

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6bd6844fb58ba85693bcf3b1c443fc585c09ed410054d7c127e477527a5aed49 oid sha256:6fddc7151694a9622baf4daf5387c47266f1bae4ba0a50bd7ee4435eb403a375
size 32131 size 33088

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:728d0e42d5e07b3fd47b8cf3f415f945fadc606de425dc0b1b2f65750a1c5e34 oid sha256:8c44fadbdeb27169f88a2678ab8b6db9ac7a9f6b21871a436d14728eec398daf
size 27059 size 27883