Merge branch 'develop' into feature/fga/user_detail_direct_chat

This commit is contained in:
ganfra 2023-11-30 18:10:51 +01:00
commit 03a2e31d46
76 changed files with 329 additions and 148 deletions

View file

@ -33,7 +33,7 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'

View file

@ -33,7 +33,7 @@ jobs:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- uses: actions/setup-java@v3 - uses: actions/setup-java@v4
name: Use JDK 17 name: Use JDK 17
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options

View file

@ -18,7 +18,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'

View file

@ -21,7 +21,7 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@v1.2.2 uses: nschloe/action-cached-lfs-checkout@v1.2.2
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
@ -57,7 +57,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'

View file

@ -35,7 +35,7 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'

View file

@ -33,7 +33,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: ☕️ Use JDK 17 - name: ☕️ Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'

View file

@ -20,7 +20,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'

View file

@ -27,7 +27,7 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: Use JDK 17 - name: Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'

View file

@ -39,7 +39,7 @@ jobs:
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- name: ☕️ Use JDK 17 - name: ☕️ Use JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'

View file

@ -376,3 +376,22 @@ subprojects {
} }
} }
} }
subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
)
}
if (project.findProperty("composeCompilerMetrics") == "true") {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
)
}
}
}
}

View file

@ -0,0 +1 @@
Make most code used in Compose from `:libraries:matrix` and derived classes Immutable or Stable.

View file

@ -0,0 +1 @@
Set a default power level to join calls. Also, create new rooms taking this power level into account.

View file

@ -79,6 +79,7 @@ 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
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.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState import io.element.android.libraries.matrix.ui.room.canRedactAsState
@ -108,6 +109,7 @@ class MessagesPresenter @AssistedInject constructor(
private val featureFlagsService: FeatureFlagService, private val featureFlagsService: FeatureFlagService,
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val currentSessionIdHolder: CurrentSessionIdHolder,
) : Presenter<MessagesState> { ) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
@ -133,10 +135,10 @@ class MessagesPresenter @AssistedInject constructor(
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value) val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
val roomName: Async<String> by remember(roomInfo?.name) { val roomName: Async<String> by remember {
derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized } derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized }
} }
val roomAvatar: Async<AvatarData> by remember(roomInfo?.avatarUrl) { val roomAvatar: Async<AvatarData> by remember {
derivedStateOf { roomInfo?.avatarData()?.let { Async.Success(it) } ?: Async.Uninitialized } derivedStateOf { roomInfo?.avatarData()?.let { Async.Success(it) } ?: Async.Uninitialized }
} }
@ -144,6 +146,16 @@ class MessagesPresenter @AssistedInject constructor(
mutableStateOf(false) mutableStateOf(false)
} }
var canJoinCall by rememberSaveable {
mutableStateOf(false)
}
LaunchedEffect(currentSessionIdHolder.current) {
withContext(dispatchers.io) {
canJoinCall = room.canUserJoinCall(userId = currentSessionIdHolder.current).getOrDefault(false)
}
}
val inviteProgress = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) } val inviteProgress = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) } var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) { LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
@ -162,8 +174,6 @@ class MessagesPresenter @AssistedInject constructor(
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
var enableVoiceMessages by remember { mutableStateOf(false) } var enableVoiceMessages by remember { mutableStateOf(false) }
// TODO add min power level to use this feature in the future?
val enableInRoomCalls = true
LaunchedEffect(featureFlagsService) { LaunchedEffect(featureFlagsService) {
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
} }
@ -193,6 +203,12 @@ class MessagesPresenter @AssistedInject constructor(
} }
} }
val callState = when {
!canJoinCall -> RoomCallState.DISABLED
roomInfo?.hasRoomCall == true -> RoomCallState.ONGOING
else -> RoomCallState.ENABLED
}
return MessagesState( return MessagesState(
roomId = room.roomId, roomId = room.roomId,
roomName = roomName, roomName = roomName,
@ -213,9 +229,8 @@ class MessagesPresenter @AssistedInject constructor(
inviteProgress = inviteProgress.value, inviteProgress = inviteProgress.value,
enableTextFormatting = enableTextFormatting, enableTextFormatting = enableTextFormatting,
enableVoiceMessages = enableVoiceMessages, enableVoiceMessages = enableVoiceMessages,
enableInRoomCalls = enableInRoomCalls,
appName = buildMeta.applicationName, appName = buildMeta.applicationName,
isCallOngoing = roomInfo?.hasRoomCall ?: false, callState = callState,
eventSink = { handleEvents(it) } eventSink = { handleEvents(it) }
) )
} }

View file

@ -51,8 +51,13 @@ data class MessagesState(
val showReinvitePrompt: Boolean, val showReinvitePrompt: Boolean,
val enableTextFormatting: Boolean, val enableTextFormatting: Boolean,
val enableVoiceMessages: Boolean, val enableVoiceMessages: Boolean,
val enableInRoomCalls: Boolean, val callState: RoomCallState,
val isCallOngoing: Boolean,
val appName: String, val appName: String,
val eventSink: (MessagesEvents) -> Unit val eventSink: (MessagesEvents) -> Unit
) )
enum class RoomCallState {
ENABLED,
ONGOING,
DISABLED
}

View file

@ -66,7 +66,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
), ),
), ),
aMessagesState().copy( aMessagesState().copy(
isCallOngoing = true, callState = RoomCallState.ONGOING,
), ),
aMessagesState().copy( aMessagesState().copy(
enableVoiceMessages = true, enableVoiceMessages = true,
@ -75,6 +75,9 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
showSendFailureDialog = true showSendFailureDialog = true
), ),
), ),
aMessagesState().copy(
callState = RoomCallState.DISABLED,
),
) )
} }
@ -117,8 +120,7 @@ fun aMessagesState() = MessagesState(
showReinvitePrompt = false, showReinvitePrompt = false,
enableTextFormatting = true, enableTextFormatting = true,
enableVoiceMessages = true, enableVoiceMessages = true,
enableInRoomCalls = true, callState = RoomCallState.ENABLED,
isCallOngoing = false,
appName = "Element", appName = "Element",
eventSink = {} eventSink = {}
) )

View file

@ -191,10 +191,9 @@ fun MessagesView(
MessagesViewTopBar( MessagesViewTopBar(
roomName = state.roomName.dataOrNull(), roomName = state.roomName.dataOrNull(),
roomAvatar = state.roomAvatar.dataOrNull(), roomAvatar = state.roomAvatar.dataOrNull(),
inRoomCallsEnabled = state.enableInRoomCalls, callState = state.callState,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked, onRoomDetailsClicked = onRoomDetailsClicked,
isCallOngoing = state.isCallOngoing,
onJoinCallClicked = onJoinCallClicked, onJoinCallClicked = onJoinCallClicked,
) )
} }
@ -449,8 +448,7 @@ private fun MessagesViewComposerBottomSheetContents(
private fun MessagesViewTopBar( private fun MessagesViewTopBar(
roomName: String?, roomName: String?,
roomAvatar: AvatarData?, roomAvatar: AvatarData?,
inRoomCallsEnabled: Boolean, callState: RoomCallState,
isCallOngoing: Boolean,
onRoomDetailsClicked: () -> Unit, onRoomDetailsClicked: () -> Unit,
onJoinCallClicked: () -> Unit, onJoinCallClicked: () -> Unit,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
@ -477,13 +475,11 @@ private fun MessagesViewTopBar(
} }
}, },
actions = { actions = {
if (inRoomCallsEnabled) { if (callState == RoomCallState.ONGOING) {
if (isCallOngoing) { JoinCallMenuItem(onJoinCallClicked = onJoinCallClicked)
JoinCallMenuItem(onJoinCallClicked = onJoinCallClicked) } else {
} else { IconButton(onClick = onJoinCallClicked, enabled = callState != RoomCallState.DISABLED) {
IconButton(onClick = onJoinCallClicked) { Icon(CompoundIcons.VideoCall, contentDescription = stringResource(CommonStrings.a11y_start_call))
Icon(CompoundIcons.VideoCall, contentDescription = stringResource(CommonStrings.a11y_start_call))
}
} }
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))

View file

@ -45,6 +45,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
@ -127,8 +129,8 @@ class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> {
question = "Poll which are being replied.", question = "Poll which are being replied.",
kind = PollKind.Disclosed, kind = PollKind.Disclosed,
maxSelections = 1u, maxSelections = 1u,
answers = emptyList(), answers = persistentListOf(),
votes = emptyMap(), votes = persistentMapOf(),
endTime = null endTime = null
), ),
).map { ).map {

View file

@ -17,7 +17,6 @@
package io.element.android.features.messages.impl.timeline.components.reactionsummary package io.element.android.features.messages.impl.timeline.components.reactionsummary
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -38,10 +37,6 @@ class ReactionSummaryPresenter @Inject constructor(
) : Presenter<ReactionSummaryState> { ) : Presenter<ReactionSummaryState> {
@Composable @Composable
override fun present(): ReactionSummaryState { override fun present(): ReactionSummaryState {
LaunchedEffect(Unit) {
room.updateMembers()
}
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val target: MutableState<ReactionSummaryState.Summary?> = remember { val target: MutableState<ReactionSummaryState.Summary?> = remember {

View file

@ -92,12 +92,14 @@ import io.element.android.tests.testutils.consumeItemsUntilTimeout
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.waitForPredicate import io.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@Suppress("LargeClass")
class MessagesPresenterTest { class MessagesPresenterTest {
@get:Rule @get:Rule
@ -125,6 +127,21 @@ class MessagesPresenterTest {
} }
} }
@Test
fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest {
val room = FakeMatrixRoom().apply {
givenCanUserJoinCall(Result.success(false))
givenRoomInfo(aRoomInfo(hasRoomCall = true))
}
val presenter = createMessagesPresenter(matrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = consumeItemsUntilTimeout().last()
assertThat(initialState.callState).isEqualTo(RoomCallState.DISABLED)
}
}
@Test @Test
fun `present - handle toggling a reaction`() = runTest { fun `present - handle toggling a reaction`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
@ -462,7 +479,7 @@ class MessagesPresenterTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID) val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
room.givenRoomMembersState( room.givenRoomMembersState(
MatrixRoomMembersState.Ready( MatrixRoomMembersState.Ready(
listOf( persistentListOf(
aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN),
aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE), aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE),
) )
@ -489,7 +506,7 @@ class MessagesPresenterTest {
room.givenRoomMembersState( room.givenRoomMembersState(
MatrixRoomMembersState.Error( MatrixRoomMembersState.Error(
failure = Throwable(), failure = Throwable(),
prevRoomMembers = listOf( prevRoomMembers = persistentListOf(
aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN),
aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE), aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE),
) )
@ -535,7 +552,7 @@ class MessagesPresenterTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID) val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
room.givenRoomMembersState( room.givenRoomMembersState(
MatrixRoomMembersState.Ready( MatrixRoomMembersState.Ready(
listOf( persistentListOf(
aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN),
aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE), aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE),
) )
@ -655,6 +672,7 @@ class MessagesPresenterTest {
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
currentSessionIdHolder: CurrentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
): MessagesPresenter { ): MessagesPresenter {
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
@ -723,6 +741,7 @@ class MessagesPresenterTest {
featureFlagsService = FakeFeatureFlagService(), featureFlagsService = FakeFeatureFlagService(),
buildMeta = aBuildMeta(), buildMeta = aBuildMeta(),
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
currentSessionIdHolder = currentSessionIdHolder,
) )
} }
} }

View file

@ -78,11 +78,11 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate import io.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.internal.immutableListOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import uniffi.wysiwyg_composer.MentionsState import uniffi.wysiwyg_composer.MentionsState
@ -734,7 +734,7 @@ class MessageComposerPresenterTest {
isOneToOne = false, isOneToOne = false,
).apply { ).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready( givenRoomMembersState(MatrixRoomMembersState.Ready(
immutableListOf(currentUser, invitedUser, bob, david), persistentListOf(currentUser, invitedUser, bob, david),
)) ))
givenCanTriggerRoomNotification(Result.success(true)) givenCanTriggerRoomNotification(Result.success(true))
} }
@ -798,7 +798,7 @@ class MessageComposerPresenterTest {
isOneToOne = true, isOneToOne = true,
).apply { ).apply {
givenRoomMembersState(MatrixRoomMembersState.Ready( givenRoomMembersState(MatrixRoomMembersState.Ready(
immutableListOf(currentUser, invitedUser, bob, david), persistentListOf(currentUser, invitedUser, bob, david),
)) ))
givenCanTriggerRoomNotification(Result.success(true)) givenCanTriggerRoomNotification(Result.success(true))
} }

View file

@ -55,6 +55,7 @@ import io.element.android.tests.testutils.awaitWithLatch
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import io.element.android.tests.testutils.waitForPredicate import io.element.android.tests.testutils.waitForPredicate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -254,18 +255,18 @@ class TimelinePresenterTest {
val (alice, bob, charlie) = aMatrixUserList().take(3).mapIndexed { i, user -> val (alice, bob, charlie) = aMatrixUserList().take(3).mapIndexed { i, user ->
ReactionSender(senderId = user.userId, timestamp = now + i * minuteInMillis) ReactionSender(senderId = user.userId, timestamp = now + i * minuteInMillis)
} }
val oneReaction = listOf( val oneReaction = persistentListOf(
EventReaction( EventReaction(
key = "❤️", key = "❤️",
senders = listOf(alice, charlie) senders = persistentListOf(alice, charlie)
), ),
EventReaction( EventReaction(
key = "👍", key = "👍",
senders = listOf(alice, bob) senders = persistentListOf(alice, bob)
), ),
EventReaction( EventReaction(
key = "🐶", key = "🐶",
senders = listOf(charlie) senders = persistentListOf(charlie)
), ),
) )
timeline.updateTimelineItems { timeline.updateTimelineItems {

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
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.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -42,7 +43,7 @@ class ReactionSummaryPresenterTests {
private val roomMember = aRoomMember(userId = A_USER_ID, avatarUrl = AN_AVATAR_URL, displayName = A_USER_NAME) private val roomMember = aRoomMember(userId = A_USER_ID, avatarUrl = AN_AVATAR_URL, displayName = A_USER_NAME)
private val summaryEvent = ReactionSummaryEvents.ShowReactionSummary(AN_EVENT_ID, listOf(aggregatedReaction), aggregatedReaction.key) private val summaryEvent = ReactionSummaryEvents.ShowReactionSummary(AN_EVENT_ID, listOf(aggregatedReaction), aggregatedReaction.key)
private val room = FakeMatrixRoom().apply { private val room = FakeMatrixRoom().apply {
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember)))
} }
private val presenter = ReactionSummaryPresenter(room) private val presenter = ReactionSummaryPresenter(room)

View file

@ -55,6 +55,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageT
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.ui.components.A_BLUR_HASH import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -280,7 +281,7 @@ class TimelineItemContentMessageFactoryTest {
), ),
details = AudioDetails( details = AudioDetails(
duration = 1.minutes, duration = 1.minutes,
waveform = listOf(1f, 2f), waveform = persistentListOf(1f, 2f),
), ),
) )
), ),
@ -293,7 +294,7 @@ class TimelineItemContentMessageFactoryTest {
duration = 1.minutes, duration = 1.minutes,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.Ogg, mimeType = MimeTypes.Ogg,
waveform = listOf(1f, 2f).toImmutableList() waveform = persistentListOf(1f, 2f)
) )
assertThat(result).isEqualTo(expected) assertThat(result).isEqualTo(expected)
} }

View file

@ -38,6 +38,11 @@ import io.element.android.libraries.matrix.test.A_USER_ID_7
import io.element.android.libraries.matrix.test.A_USER_ID_8 import io.element.android.libraries.matrix.test.A_USER_ID_8
import io.element.android.libraries.matrix.test.A_USER_ID_9 import io.element.android.libraries.matrix.test.A_USER_ID_9
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -55,7 +60,7 @@ internal class TimelineItemContentPollFactoryTest {
@Test @Test
fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest { fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest {
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
Truth.assertThat( Truth.assertThat(
factory.create(aPollContent(votes = votes), eventId = null) factory.create(aPollContent(votes = votes), eventId = null)
) )
@ -87,7 +92,7 @@ internal class TimelineItemContentPollFactoryTest {
@Test @Test
fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest { fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
Truth.assertThat( Truth.assertThat(
factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null) factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null)
) )
@ -106,7 +111,7 @@ internal class TimelineItemContentPollFactoryTest {
@Test @Test
fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
Truth.assertThat( Truth.assertThat(
factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null) factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null)
) )
@ -136,7 +141,7 @@ internal class TimelineItemContentPollFactoryTest {
@Test @Test
fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest { fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest {
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
Truth.assertThat( Truth.assertThat(
factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes), eventId = null) factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes), eventId = null)
) )
@ -172,7 +177,7 @@ internal class TimelineItemContentPollFactoryTest {
@Test @Test
fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest { fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
Truth.assertThat( Truth.assertThat(
factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL), eventId = null) factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL), eventId = null)
) )
@ -192,7 +197,7 @@ internal class TimelineItemContentPollFactoryTest {
@Test @Test
fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
Truth.assertThat( Truth.assertThat(
factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL), eventId = null) factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL), eventId = null)
) )
@ -221,13 +226,13 @@ internal class TimelineItemContentPollFactoryTest {
private fun aPollContent( private fun aPollContent(
pollKind: PollKind = PollKind.Disclosed, pollKind: PollKind = PollKind.Disclosed,
votes: Map<String, List<UserId>> = emptyMap(), votes: ImmutableMap<String, ImmutableList<UserId>> = persistentMapOf(),
endTime: ULong? = null, endTime: ULong? = null,
): PollContent = PollContent( ): PollContent = PollContent(
question = A_POLL_QUESTION, question = A_POLL_QUESTION,
kind = pollKind, kind = pollKind,
maxSelections = 1UL, maxSelections = 1UL,
answers = listOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4), answers = persistentListOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4),
votes = votes, votes = votes,
endTime = endTime, endTime = endTime,
) )
@ -275,17 +280,17 @@ internal class TimelineItemContentPollFactoryTest {
private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries") private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries")
private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger") private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger")
private val MY_USER_WINNING_VOTES = mapOf( private val MY_USER_WINNING_VOTES = persistentMapOf(
A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4),
A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner
A_POLL_ANSWER_3 to emptyList(), A_POLL_ANSWER_3 to persistentListOf(),
A_POLL_ANSWER_4 to listOf(A_USER_ID_10), A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_10),
) )
private val OTHER_WINNING_VOTES = mapOf( private val OTHER_WINNING_VOTES = persistentMapOf(
A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner
A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_6), A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_6),
A_POLL_ANSWER_3 to emptyList(), A_POLL_ANSWER_3 to persistentListOf(),
A_POLL_ANSWER_4 to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner
) )
} }
} }

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -88,8 +89,8 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
isOwn = false, isOwn = false,
isRemote = false, isRemote = false,
localSendState = null, localSendState = null,
reactions = listOf(), reactions = persistentListOf(),
receipts = listOf(), receipts = persistentListOf(),
sender = A_USER_ID, sender = A_USER_ID,
senderProfile = ProfileTimelineDetails.Unavailable, senderProfile = ProfileTimelineDetails.Unavailable,
timestamp = 9442, timestamp = 9442,

View file

@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.test.room.anEventTimelineItem
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -539,7 +540,7 @@ class CreatePollPresenterTest {
private fun anExistingPoll() = aPollContent( private fun anExistingPoll() = aPollContent(
question = "Do you like polls?", question = "Do you like polls?",
answers = listOf( answers = persistentListOf(
PollAnswer("1", "Yes"), PollAnswer("1", "Yes"),
PollAnswer("2", "No"), PollAnswer("2", "No"),
PollAnswer("2", "Maybe"), PollAnswer("2", "Maybe"),

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
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 kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@ -84,7 +85,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
return EditDefaultNotificationSettingState( return EditDefaultNotificationSettingState(
isOneToOne = isOneToOne, isOneToOne = isOneToOne,
mode = mode.value, mode = mode.value,
roomsWithUserDefinedMode = roomsWithUserDefinedMode.value, roomsWithUserDefinedMode = roomsWithUserDefinedMode.value.toImmutableList(),
changeNotificationSettingAction = changeNotificationSettingAction.value, changeNotificationSettingAction = changeNotificationSettingAction.value,
eventSink = ::handleEvents eventSink = ::handleEvents
) )

View file

@ -19,11 +19,12 @@ package io.element.android.features.preferences.impl.notifications.edit
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
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 io.element.android.libraries.matrix.api.roomlist.RoomSummary
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: List<RoomSummary.Filled>, val roomsWithUserDefinedMode: ImmutableList<RoomSummary.Filled>,
val changeNotificationSettingAction: Async<Unit>, val changeNotificationSettingAction: Async<Unit>,
val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit,
) )

View file

@ -22,6 +22,7 @@ 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.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import kotlinx.collections.immutable.persistentListOf
open class EditDefaultNotificationSettingStateProvider: PreviewParameterProvider<EditDefaultNotificationSettingState> { open class EditDefaultNotificationSettingStateProvider: PreviewParameterProvider<EditDefaultNotificationSettingState> {
override val values: Sequence<EditDefaultNotificationSettingState> override val values: Sequence<EditDefaultNotificationSettingState>
@ -39,7 +40,7 @@ private fun anEditDefaultNotificationSettingsState(
) = EditDefaultNotificationSettingState( ) = EditDefaultNotificationSettingState(
isOneToOne = isOneToOne, isOneToOne = isOneToOne,
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
roomsWithUserDefinedMode = listOf(aRoomSummary()), roomsWithUserDefinedMode = persistentListOf(aRoomSummary()),
changeNotificationSettingAction = changeNotificationSettingAction, changeNotificationSettingAction = changeNotificationSettingAction,
eventSink = {} eventSink = {}
) )

View file

@ -90,7 +90,7 @@ fun aRoomMember(
isIgnored = isIgnored, isIgnored = isIgnored,
) )
fun aRoomMemberList() = listOf( fun aRoomMemberList() = persistentListOf(
anAlice(), anAlice(),
aBob(), aBob(),
aRoomMember(UserId("@carol:server.org"), "Carol"), aRoomMember(UserId("@carol:server.org"), "Carol"),

View file

@ -49,6 +49,7 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -129,7 +130,7 @@ class RoomDetailsPresenterTests {
isEncrypted = true, isEncrypted = true,
isDirect = true, isDirect = true,
).apply { ).apply {
val roomMembers = listOf(myRoomMember, otherRoomMember) val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
} }
val presenter = createRoomDetailsPresenter(room) val presenter = createRoomDetailsPresenter(room)
@ -220,7 +221,7 @@ class RoomDetailsPresenterTests {
isEncrypted = true, isEncrypted = true,
isDirect = true, isDirect = true,
).apply { ).apply {
val roomMembers = listOf(myRoomMember, otherRoomMember) val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
@ -253,7 +254,7 @@ class RoomDetailsPresenterTests {
isDirect = true, isDirect = true,
topic = null, topic = null,
).apply { ).apply {
val roomMembers = listOf(myRoomMember, otherRoomMember) val roomMembers = persistentListOf(myRoomMember, otherRoomMember)
givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))

View file

@ -39,6 +39,7 @@ import io.element.android.libraries.usersearch.test.FakeUserRepository
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
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -167,7 +168,7 @@ internal class RoomInviteMembersPresenterTest {
matrixRoom = FakeMatrixRoom().apply { matrixRoom = FakeMatrixRoom().apply {
givenRoomMembersState( givenRoomMembersState(
MatrixRoomMembersState.Ready( MatrixRoomMembersState.Ready(
listOf( persistentListOf(
aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN),
aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE),
) )
@ -227,7 +228,7 @@ internal class RoomInviteMembersPresenterTest {
roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply { roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply {
givenRoomMembersState( givenRoomMembersState(
MatrixRoomMembersState.Ready( MatrixRoomMembersState.Ready(
listOf( persistentListOf(
aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN),
aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE),
) )

View file

@ -39,6 +39,7 @@ 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.FakeMatrixClient
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -56,7 +57,7 @@ class RoomMemberDetailsPresenterTests {
val room = aMatrixRoom().apply { val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.success("A custom name")) givenUserDisplayNameResult(Result.success("A custom name"))
givenUserAvatarUrlResult(Result.success("A custom avatar")) givenUserAvatarUrlResult(Result.success("A custom avatar"))
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember)))
} }
val presenter = createRoomMemberDetailsPresenter( val presenter = createRoomMemberDetailsPresenter(
room = room, room = room,
@ -83,7 +84,7 @@ class RoomMemberDetailsPresenterTests {
val room = aMatrixRoom().apply { val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.failure(Throwable())) givenUserDisplayNameResult(Result.failure(Throwable()))
givenUserAvatarUrlResult(Result.failure(Throwable())) givenUserAvatarUrlResult(Result.failure(Throwable()))
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember)))
} }
val presenter = createRoomMemberDetailsPresenter( val presenter = createRoomMemberDetailsPresenter(
room = room, room = room,
@ -106,7 +107,7 @@ class RoomMemberDetailsPresenterTests {
val room = aMatrixRoom().apply { val room = aMatrixRoom().apply {
givenUserDisplayNameResult(Result.success(null)) givenUserDisplayNameResult(Result.success(null))
givenUserAvatarUrlResult(Result.success(null)) givenUserAvatarUrlResult(Result.success(null))
givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember)))
} }
val presenter = createRoomMemberDetailsPresenter( val presenter = createRoomMemberDetailsPresenter(
room = room, room = room,

View file

@ -23,7 +23,7 @@ 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
@Immutable @Immutable
data class RoomListRoomSummary constructor( data class RoomListRoomSummary(
val id: String, val id: String,
val roomId: RoomId, val roomId: RoomId,
val name: String = "", val name: String = "",

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -132,7 +133,7 @@ class VerifySelfSessionPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
requestVerificationAndAwaitVerifyingState(service) requestVerificationAndAwaitVerifyingState(service)
service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(emptyList())) service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(persistentListOf()))
ensureAllEventsConsumed() ensureAllEventsConsumed()
} }
} }

View file

@ -37,7 +37,7 @@ serialization_json = "1.6.1"
showkase = "1.0.2" showkase = "1.0.2"
appyx = "1.4.0" appyx = "1.4.0"
sqldelight = "2.0.0" sqldelight = "2.0.0"
wysiwyg = "2.19.0" wysiwyg = "2.20.0"
# DI # DI
dagger = "2.48.1" dagger = "2.48.1"
@ -89,7 +89,7 @@ androidx_preference = "androidx.preference:preference:1.2.1"
androidx_webkit = "androidx.webkit:webkit:1.8.0" androidx_webkit = "androidx.webkit:webkit:1.8.0"
androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" }
androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha11" androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha12"
androidx_compose_ui = { module = "androidx.compose.ui:ui" } androidx_compose_ui = { module = "androidx.compose.ui:ui" }
androidx_compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" } androidx_compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" }
@ -147,7 +147,7 @@ jsoup = "org.jsoup:jsoup:1.17.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.1" molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.1"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.71" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.72"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=f2b9ed0faf8472cbe469255ae6c86eddb77076c75191741b4a462f33128dd419 distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View file

@ -16,6 +16,9 @@
package io.element.android.libraries.matrix.api.encryption package io.element.android.libraries.matrix.api.encryption
import androidx.compose.runtime.Immutable
@Immutable
sealed interface BackupUploadState { sealed interface BackupUploadState {
data object Unknown : BackupUploadState data object Unknown : BackupUploadState

View file

@ -16,6 +16,9 @@
package io.element.android.libraries.matrix.api.encryption package io.element.android.libraries.matrix.api.encryption
import androidx.compose.runtime.Immutable
@Immutable
sealed interface SteadyStateException { sealed interface SteadyStateException {
/** /**
* The backup can be deleted. * The backup can be deleted.

View file

@ -16,9 +16,10 @@
package io.element.android.libraries.matrix.api.media package io.element.android.libraries.matrix.api.media
import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration import kotlin.time.Duration
data class AudioDetails( data class AudioDetails(
val duration: Duration, val duration: Duration,
val waveform: List<Float>, val waveform: ImmutableList<Float>,
) )

View file

@ -17,18 +17,21 @@
package io.element.android.libraries.matrix.api.permalink package io.element.android.libraries.matrix.api.permalink
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
/** /**
* This sealed class represents all the permalink cases. * This sealed class represents all the permalink cases.
* You don't have to instantiate yourself but should use [PermalinkParser] instead. * You don't have to instantiate yourself but should use [PermalinkParser] instead.
*/ */
@Immutable
sealed interface PermalinkData { sealed interface PermalinkData {
data class RoomLink( data class RoomLink(
val roomIdOrAlias: String, val roomIdOrAlias: String,
val isRoomAlias: Boolean, val isRoomAlias: Boolean,
val eventId: String?, val eventId: String?,
val viaParameters: List<String> val viaParameters: ImmutableList<String>
) : PermalinkData ) : PermalinkData
/* /*

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.api.permalink
import android.net.Uri import android.net.Uri
import android.net.UrlQuerySanitizer import android.net.UrlQuerySanitizer
import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.MatrixPatterns
import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber import timber.log.Timber
import java.net.URLDecoder import java.net.URLDecoder
@ -80,7 +81,7 @@ object PermalinkParser {
roomIdOrAlias = decodedIdentifier, roomIdOrAlias = decodedIdentifier,
isRoomAlias = true, isRoomAlias = true,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters viaParameters = viaQueryParameters.toImmutableList()
) )
} }
else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier)) else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier))
@ -119,7 +120,7 @@ object PermalinkParser {
roomIdOrAlias = identifier, roomIdOrAlias = identifier,
isRoomAlias = false, isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters viaParameters = viaQueryParameters.toImmutableList()
) )
} }
} }

View file

@ -132,6 +132,8 @@ interface MatrixRoom : Closeable {
suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean> suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean>
suspend fun canUserJoinCall(userId: UserId): Result<Boolean>
suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit>
suspend fun removeAvatar(): Result<Unit> suspend fun removeAvatar(): Result<Unit>

View file

@ -16,8 +16,11 @@
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class MatrixRoomInfo( data class MatrixRoomInfo(
val id: String, val id: String,
val name: String?, val name: String?,
@ -28,7 +31,7 @@ data class MatrixRoomInfo(
val isSpace: Boolean, val isSpace: Boolean,
val isTombstoned: Boolean, val isTombstoned: Boolean,
val canonicalAlias: String?, val canonicalAlias: String?,
val alternativeAliases: List<String>, val alternativeAliases: ImmutableList<String>,
val currentUserMembership: CurrentUserMembership, val currentUserMembership: CurrentUserMembership,
val latestEvent: EventTimelineItem?, val latestEvent: EventTimelineItem?,
val inviter: RoomMember?, val inviter: RoomMember?,
@ -39,5 +42,5 @@ data class MatrixRoomInfo(
val notificationCount: Long, val notificationCount: Long,
val userDefinedNotificationMode: RoomNotificationMode?, val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean, val hasRoomCall: Boolean,
val activeRoomCallParticipants: List<String> val activeRoomCallParticipants: ImmutableList<String>
) )

View file

@ -17,13 +17,14 @@
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable
sealed interface MatrixRoomMembersState { sealed interface MatrixRoomMembersState {
data object Unknown : MatrixRoomMembersState data object Unknown : MatrixRoomMembersState
data class Pending(val prevRoomMembers: List<RoomMember>? = null) : MatrixRoomMembersState data class Pending(val prevRoomMembers: ImmutableList<RoomMember>? = null) : MatrixRoomMembersState
data class Error(val failure: Throwable, val prevRoomMembers: List<RoomMember>? = null) : MatrixRoomMembersState data class Error(val failure: Throwable, val prevRoomMembers: ImmutableList<RoomMember>? = null) : MatrixRoomMembersState
data class Ready(val roomMembers: List<RoomMember>) : MatrixRoomMembersState data class Ready(val roomMembers: ImmutableList<RoomMember>) : MatrixRoomMembersState
} }
fun MatrixRoomMembersState.roomMembers(): List<RoomMember>? { fun MatrixRoomMembersState.roomMembers(): List<RoomMember>? {

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.api.roomlist package io.element.android.libraries.matrix.api.roomlist
import androidx.compose.runtime.Immutable
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
/** /**
@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.StateFlow
*/ */
interface RoomListService { interface RoomListService {
@Immutable
sealed interface State { sealed interface State {
data object Idle : State data object Idle : State
data object Running : State data object Running : State
@ -32,6 +34,7 @@ interface RoomListService {
data object Terminated : State data object Terminated : State
} }
@Immutable
sealed interface SyncIndicator { sealed interface SyncIndicator {
data object Show : SyncIndicator data object Show : SyncIndicator
data object Hide : SyncIndicator data object Hide : SyncIndicator

View file

@ -16,11 +16,15 @@
package io.element.android.libraries.matrix.api.timeline.item.event package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
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.media.ImageInfo import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@Immutable
sealed interface EventContent sealed interface EventContent
data class MessageContent( data class MessageContent(
@ -43,14 +47,16 @@ data class PollContent(
val question: String, val question: String,
val kind: PollKind, val kind: PollKind,
val maxSelections: ULong, val maxSelections: ULong,
val answers: List<PollAnswer>, val answers: ImmutableList<PollAnswer>,
val votes: Map<String, List<UserId>>, val votes: ImmutableMap<String, ImmutableList<UserId>>,
val endTime: ULong? val endTime: ULong?
) : EventContent ) : EventContent
data class UnableToDecryptContent( data class UnableToDecryptContent(
val data: Data val data: Data
) : EventContent { ) : EventContent {
@Immutable
sealed interface Data { sealed interface Data {
data class OlmV1Curve25519AesSha2( data class OlmV1Curve25519AesSha2(
val senderKey: String val senderKey: String

View file

@ -16,7 +16,11 @@
package io.element.android.libraries.matrix.api.timeline.item.event package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class EventReaction( data class EventReaction(
val key: String, val key: String,
val senders: List<ReactionSender> val senders: ImmutableList<ReactionSender>
) )

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.TransactionId
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.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import kotlinx.collections.immutable.ImmutableList
data class EventTimelineItem( data class EventTimelineItem(
val eventId: EventId?, val eventId: EventId?,
@ -29,8 +30,8 @@ data class EventTimelineItem(
val isOwn: Boolean, val isOwn: Boolean,
val isRemote: Boolean, val isRemote: Boolean,
val localSendState: LocalEventSendState?, val localSendState: LocalEventSendState?,
val reactions: List<EventReaction>, val reactions: ImmutableList<EventReaction>,
val receipts: List<Receipt>, val receipts: ImmutableList<Receipt>,
val sender: UserId, val sender: UserId,
val senderProfile: ProfileTimelineDetails, val senderProfile: ProfileTimelineDetails,
val timestamp: Long, val timestamp: Long,

View file

@ -16,9 +16,11 @@
package io.element.android.libraries.matrix.api.timeline.item.event package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@Immutable
sealed interface InReplyTo { sealed interface InReplyTo {
/** The event details are not loaded yet. We can fetch them. */ /** The event details are not loaded yet. We can fetch them. */
data class NotLoaded(val eventId: EventId) : InReplyTo data class NotLoaded(val eventId: EventId) : InReplyTo

View file

@ -16,8 +16,10 @@
package io.element.android.libraries.matrix.api.timeline.item.event package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@Immutable
sealed interface LocalEventSendState { sealed interface LocalEventSendState {
data object NotSentYet : LocalEventSendState data object NotSentYet : LocalEventSendState
data object Canceled : LocalEventSendState data object Canceled : LocalEventSendState

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.api.timeline.item.event package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.media.AudioDetails import io.element.android.libraries.matrix.api.media.AudioDetails
import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.FileInfo
@ -23,6 +24,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.media.VideoInfo
@Immutable
sealed interface MessageType sealed interface MessageType
data class EmoteMessageType( data class EmoteMessageType(

View file

@ -16,6 +16,9 @@
package io.element.android.libraries.matrix.api.timeline.item.event package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
@Immutable
sealed interface OtherState { sealed interface OtherState {
data object PolicyRuleRoom : OtherState data object PolicyRuleRoom : OtherState
data object PolicyRuleServer : OtherState data object PolicyRuleServer : OtherState

View file

@ -16,6 +16,9 @@
package io.element.android.libraries.matrix.api.timeline.item.event package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
@Immutable
sealed interface ProfileTimelineDetails { sealed interface ProfileTimelineDetails {
data object Unavailable : ProfileTimelineDetails data object Unavailable : ProfileTimelineDetails

View file

@ -16,7 +16,9 @@
package io.element.android.libraries.matrix.api.user package io.element.android.libraries.matrix.api.user
import kotlinx.collections.immutable.ImmutableList
data class MatrixSearchUserResults( data class MatrixSearchUserResults(
val results: List<MatrixUser>, val results: ImmutableList<MatrixUser>,
val limited: Boolean, val limited: Boolean,
) )

View file

@ -16,6 +16,8 @@
package io.element.android.libraries.matrix.api.verification package io.element.android.libraries.matrix.api.verification
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -75,6 +77,7 @@ interface SessionVerificationService {
} }
/** Verification status of the current session. */ /** Verification status of the current session. */
@Immutable
sealed interface SessionVerifiedStatus { sealed interface SessionVerifiedStatus {
/** Unknown status, we couldn't read the actual value from the SDK. */ /** Unknown status, we couldn't read the actual value from the SDK. */
data object Unknown : SessionVerifiedStatus data object Unknown : SessionVerifiedStatus
@ -87,6 +90,7 @@ sealed interface SessionVerifiedStatus {
} }
/** States produced by the [SessionVerificationService]. */ /** States produced by the [SessionVerificationService]. */
@Immutable
sealed interface VerificationFlowState { sealed interface VerificationFlowState {
/** Initial state. */ /** Initial state. */
data object Initial : VerificationFlowState data object Initial : VerificationFlowState
@ -98,7 +102,7 @@ sealed interface VerificationFlowState {
data object StartedSasVerification : VerificationFlowState data object StartedSasVerification : VerificationFlowState
/** Verification data for the SAS verification (emojis) received. */ /** Verification data for the SAS verification (emojis) received. */
data class ReceivedVerificationData(val emoji: List<VerificationEmoji>) : VerificationFlowState data class ReceivedVerificationData(val emoji: ImmutableList<VerificationEmoji>) : VerificationFlowState
/** Verification completed successfully. */ /** Verification completed successfully. */
data object Finished : VerificationFlowState data object Finished : VerificationFlowState

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.api.permalink package io.element.android.libraries.matrix.api.permalink
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.collections.immutable.persistentListOf
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
@ -66,7 +67,7 @@ class PermalinkParserTest {
roomIdOrAlias = "!aBCD1234:matrix.org", roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false, isRoomAlias = false,
eventId = null, eventId = null,
viaParameters = emptyList(), viaParameters = persistentListOf(),
) )
) )
} }
@ -79,7 +80,7 @@ class PermalinkParserTest {
roomIdOrAlias = "!aBCD1234:matrix.org", roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false, isRoomAlias = false,
eventId = "\$1234567890abcdef:matrix.org", eventId = "\$1234567890abcdef:matrix.org",
viaParameters = emptyList(), viaParameters = persistentListOf(),
) )
) )
} }
@ -92,7 +93,7 @@ class PermalinkParserTest {
roomIdOrAlias = "!aBCD1234:matrix.org", roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false, isRoomAlias = false,
eventId = null, eventId = null,
viaParameters = emptyList(), viaParameters = persistentListOf(),
) )
) )
} }
@ -105,7 +106,7 @@ class PermalinkParserTest {
roomIdOrAlias = "!aBCD1234:matrix.org", roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false, isRoomAlias = false,
eventId = "\$1234567890abcdef:matrix.org", eventId = "\$1234567890abcdef:matrix.org",
viaParameters = listOf("matrix.org", "matrix.com"), viaParameters = persistentListOf("matrix.org", "matrix.com"),
) )
) )
} }
@ -118,7 +119,7 @@ class PermalinkParserTest {
roomIdOrAlias = "#element-android:matrix.org", roomIdOrAlias = "#element-android:matrix.org",
isRoomAlias = true, isRoomAlias = true,
eventId = null, eventId = null,
viaParameters = emptyList(), viaParameters = persistentListOf(),
) )
) )
} }

View file

@ -47,6 +47,7 @@ dependencies {
implementation("net.java.dev.jna:jna:5.13.0@aar") implementation("net.java.dev.jna:jna:5.13.0@aar")
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.serialization.json) implementation(libs.serialization.json)
implementation(libs.kotlinx.collections.immutable)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)

View file

@ -77,6 +77,7 @@ import org.matrix.rustcomponents.sdk.BackupState
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.TaskHandle
@ -274,6 +275,7 @@ class RustMatrixClient constructor(
}, },
invite = createRoomParams.invite?.map { it.value }, invite = createRoomParams.invite?.map { it.value },
avatar = createRoomParams.avatar, avatar = createRoomParams.avatar,
powerLevelContentOverride = defaultRoomCreationPowerLevels,
) )
val roomId = RoomId(client.createRoom(rustParams)) val roomId = RoomId(client.createRoom(rustParams))
@ -296,7 +298,7 @@ class RustMatrixClient constructor(
isDirect = true, isDirect = true,
visibility = RoomVisibility.PRIVATE, visibility = RoomVisibility.PRIVATE,
preset = RoomPreset.TRUSTED_PRIVATE_CHAT, preset = RoomPreset.TRUSTED_PRIVATE_CHAT,
invite = listOf(userId) invite = listOf(userId),
) )
return createRoom(createRoomParams) return createRoom(createRoomParams)
} }
@ -481,3 +483,18 @@ class RustMatrixClient constructor(
} }
} }
private val defaultRoomCreationPowerLevels = PowerLevels(
usersDefault = null,
eventsDefault = null,
stateDefault = null,
ban = null,
kick = null,
redact = null,
invite = null,
notifications = null,
users = mapOf(),
events = mapOf(
"m.call.member" to 0,
"org.matrix.msc3401.call.member" to 0,
)
)

View file

@ -17,13 +17,14 @@
package io.element.android.libraries.matrix.impl.media package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.matrix.api.media.AudioDetails import io.element.android.libraries.matrix.api.media.AudioDetails
import kotlinx.collections.immutable.toImmutableList
import kotlin.time.toJavaDuration import kotlin.time.toJavaDuration
import kotlin.time.toKotlinDuration import kotlin.time.toKotlinDuration
import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent as RustAudioDetails import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent as RustAudioDetails
fun RustAudioDetails.map(): AudioDetails = AudioDetails( fun RustAudioDetails.map(): AudioDetails = AudioDetails(
duration = duration.toKotlinDuration(), duration = duration.toKotlinDuration(),
waveform = waveform.fromMSC3246range(), waveform = waveform.fromMSC3246range().toImmutableList(),
) )
fun AudioDetails.map(): RustAudioDetails = RustAudioDetails( fun AudioDetails.map(): RustAudioDetails = RustAudioDetails(

View file

@ -20,6 +20,7 @@ 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.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.Membership as RustMembership import org.matrix.rustcomponents.sdk.Membership as RustMembership
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
@ -40,7 +41,7 @@ class MatrixRoomInfoMapper(
isSpace = it.isSpace, isSpace = it.isSpace,
isTombstoned = it.isTombstoned, isTombstoned = it.isTombstoned,
canonicalAlias = it.canonicalAlias, canonicalAlias = it.canonicalAlias,
alternativeAliases = it.alternativeAliases, alternativeAliases = it.alternativeAliases.toImmutableList(),
currentUserMembership = it.membership.map(), currentUserMembership = it.membership.map(),
latestEvent = it.latestEvent?.use (timelineItemMapper::map), latestEvent = it.latestEvent?.use (timelineItemMapper::map),
inviter = it.inviter?.use(RoomMemberMapper::map), inviter = it.inviter?.use(RoomMemberMapper::map),
@ -51,7 +52,7 @@ class MatrixRoomInfoMapper(
notificationCount = it.notificationCount.toLong(), notificationCount = it.notificationCount.toLong(),
userDefinedNotificationMode = it.userDefinedNotificationMode?.map(), userDefinedNotificationMode = it.userDefinedNotificationMode?.map(),
hasRoomCall = it.hasRoomCall, hasRoomCall = it.hasRoomCall,
activeRoomCallParticipants = it.activeRoomCallParticipants activeRoomCallParticipants = it.activeRoomCallParticipants.toImmutableList()
) )
} }
} }

View file

@ -57,6 +57,7 @@ import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -198,7 +199,7 @@ class RustMatrixRoom(
override suspend fun updateMembers(): Result<Unit> = withContext(roomMembersDispatcher) { override suspend fun updateMembers(): Result<Unit> = withContext(roomMembersDispatcher) {
val currentState = _membersStateFlow.value val currentState = _membersStateFlow.value
val currentMembers = currentState.roomMembers() val currentMembers = currentState.roomMembers()?.toImmutableList()
_membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers) _membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers)
var rustMembers: List<RoomMember>? = null var rustMembers: List<RoomMember>? = null
try { try {
@ -213,7 +214,7 @@ class RustMatrixRoom(
} }
} }
val mappedMembers = rustMembers.parallelMap(RoomMemberMapper::map) val mappedMembers = rustMembers.parallelMap(RoomMemberMapper::map)
_membersStateFlow.value = MatrixRoomMembersState.Ready(mappedMembers) _membersStateFlow.value = MatrixRoomMembersState.Ready(mappedMembers.toImmutableList())
Result.success(Unit) Result.success(Unit)
} catch (exception: CancellationException) { } catch (exception: CancellationException) {
_membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = exception) _membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = exception)
@ -363,6 +364,12 @@ class RustMatrixRoom(
} }
} }
override suspend fun canUserJoinCall(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserSendState(userId.value, StateEventType.ROOM_MEMBER_EVENT.map())
}
}
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> { override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> {
return sendAttachment(listOf(file, thumbnailFile)) { return sendAttachment(listOf(file, thumbnailFile)) {
innerTimeline.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher()) innerTimeline.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher())

View file

@ -27,6 +27,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimeli
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.Reaction
import org.matrix.rustcomponents.sdk.EventItemOrigin as RustEventItemOrigin import org.matrix.rustcomponents.sdk.EventItemOrigin as RustEventItemOrigin
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
@ -81,7 +84,7 @@ fun RustEventSendState?.map(): LocalEventSendState? {
} }
} }
private fun List<Reaction>?.map(): List<EventReaction> { private fun List<Reaction>?.map(): ImmutableList<EventReaction> {
return this?.map { return this?.map {
EventReaction( EventReaction(
key = it.key, key = it.key,
@ -90,18 +93,20 @@ private fun List<Reaction>?.map(): List<EventReaction> {
senderId = UserId(sender.senderId), senderId = UserId(sender.senderId),
timestamp = sender.timestamp.toLong() timestamp = sender.timestamp.toLong()
) )
} }.toImmutableList()
) )
} ?: emptyList() }?.toImmutableList() ?: persistentListOf()
} }
private fun Map<String, RustReceipt>.map(): List<Receipt> { private fun Map<String, RustReceipt>.map(): ImmutableList<Receipt> {
return map { return map {
Receipt( Receipt(
userId = UserId(it.key), userId = UserId(it.key),
timestamp = it.value.timestamp?.toLong() ?: 0 timestamp = it.value.timestamp?.toLong() ?: 0
) )
}.sortedByDescending { it.timestamp } }
.sortedByDescending { it.timestamp }
.toImmutableList()
} }
private fun RustEventTimelineItemDebugInfo.map(): TimelineItemDebugInfo { private fun RustEventTimelineItemDebugInfo.map(): TimelineItemDebugInfo {

View file

@ -32,6 +32,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.poll.map import io.element.android.libraries.matrix.impl.poll.map
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import org.matrix.rustcomponents.sdk.TimelineItemContent import org.matrix.rustcomponents.sdk.TimelineItemContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
@ -106,10 +108,10 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
question = kind.question, question = kind.question,
kind = kind.kind.map(), kind = kind.kind.map(),
maxSelections = kind.maxSelections, maxSelections = kind.maxSelections,
answers = kind.answers.map { answer -> answer.map() }, answers = kind.answers.map { answer -> answer.map() }.toImmutableList(),
votes = kind.votes.mapValues { vote -> votes = kind.votes.mapValues { vote ->
vote.value.map { userId -> UserId(userId) } vote.value.map { userId -> UserId(userId) }.toImmutableList()
}, }.toImmutableMap(),
endTime = kind.endTime, endTime = kind.endTime,
) )
} }

View file

@ -17,13 +17,14 @@
package io.element.android.libraries.matrix.impl.usersearch package io.element.android.libraries.matrix.impl.usersearch
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.SearchUsersResults import org.matrix.rustcomponents.sdk.SearchUsersResults
object UserSearchResultMapper { object UserSearchResultMapper {
fun map(result: SearchUsersResults): MatrixSearchUserResults { fun map(result: SearchUsersResults): MatrixSearchUserResults {
return MatrixSearchUserResults( return MatrixSearchUserResults(
results = result.results.map(UserProfileMapper::map), results = result.results.map(UserProfileMapper::map).toImmutableList(),
limited = result.limited, limited = result.limited,
) )
} }

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.sync.RustSyncService
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -106,8 +107,9 @@ class RustSessionVerificationService(
override fun didReceiveVerificationData(data: List<SessionVerificationEmoji>) { override fun didReceiveVerificationData(data: List<SessionVerificationEmoji>) {
val emojis = data.map { emoji -> val emojis = data.map { emoji ->
emoji.use { VerificationEmoji(it.symbol(), it.description()) } emoji.use { VerificationEmoji(it.symbol(), it.description()) }
} }
.toImmutableList()
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojis) _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojis)
} }

View file

@ -28,4 +28,5 @@ dependencies {
api(libs.coroutines.core) api(libs.coroutines.core)
implementation(libs.coroutines.test) implementation(libs.coroutines.test)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)
implementation(libs.kotlinx.collections.immutable)
} }

View file

@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
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
@ -110,6 +111,7 @@ class FakeMatrixRoom(
private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io") private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io")
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver()) private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true) private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
var sendMessageMentions = emptyList<Mention>() var sendMessageMentions = emptyList<Mention>()
val editMessageCalls = mutableListOf<Pair<String, String?>>() val editMessageCalls = mutableListOf<Pair<String, String?>>()
@ -291,6 +293,10 @@ class FakeMatrixRoom(
return canUserTriggerRoomNotificationResult return canUserTriggerRoomNotificationResult
} }
override suspend fun canUserJoinCall(userId: UserId): Result<Boolean> {
return canUserJoinCallResult
}
override suspend fun sendImage( override suspend fun sendImage(
file: File, file: File,
thumbnailFile: File, thumbnailFile: File,
@ -473,6 +479,10 @@ class FakeMatrixRoom(
canUserTriggerRoomNotificationResult = result canUserTriggerRoomNotificationResult = result
} }
fun givenCanUserJoinCall(result: Result<Boolean>) {
canUserJoinCallResult = result
}
fun givenIgnoreResult(result: Result<Unit>) { fun givenIgnoreResult(result: Result<Unit>) {
ignoreResult = result ignoreResult = result
} }
@ -615,7 +625,7 @@ fun aRoomInfo(
isSpace = isSpace, isSpace = isSpace,
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases, alternativeAliases = alternativeAliases.toImmutableList(),
currentUserMembership = currentUserMembership, currentUserMembership = currentUserMembership,
latestEvent = latestEvent, latestEvent = latestEvent,
inviter = inviter, inviter = inviter,
@ -626,5 +636,5 @@ fun aRoomInfo(
notificationCount = notificationCount, notificationCount = notificationCount,
userDefinedNotificationMode = userDefinedNotificationMode, userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall, hasRoomCall = hasRoomCall,
activeRoomCallParticipants = activeRoomCallParticipants activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
) )

View file

@ -44,6 +44,9 @@ 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_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
fun aRoomSummaryFilled( fun aRoomSummaryFilled(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
@ -107,8 +110,8 @@ fun anEventTimelineItem(
isOwn: Boolean = false, isOwn: Boolean = false,
isRemote: Boolean = false, isRemote: Boolean = false,
localSendState: LocalEventSendState? = null, localSendState: LocalEventSendState? = null,
reactions: List<EventReaction> = emptyList(), reactions: ImmutableList<EventReaction> = persistentListOf(),
receipts: List<Receipt> = emptyList(), receipts: ImmutableList<Receipt> = persistentListOf(),
sender: UserId = A_USER_ID, sender: UserId = A_USER_ID,
senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(), senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(),
timestamp: Long = 0L, timestamp: Long = 0L,
@ -181,12 +184,12 @@ fun aTimelineItemDebugInfo(
fun aPollContent( fun aPollContent(
question: String = "Do you like polls?", question: String = "Do you like polls?",
answers: List<PollAnswer> = listOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")), answers: ImmutableList<PollAnswer> = persistentListOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")),
) = PollContent( ) = PollContent(
question = question, question = question,
kind = PollKind.Disclosed, kind = PollKind.Disclosed,
maxSelections = 1u, maxSelections = 1u,
answers = answers, answers = answers,
votes = mapOf(), votes = persistentMapOf(),
endTime = null endTime = null
) )

View file

@ -20,6 +20,8 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -29,7 +31,7 @@ class FakeSessionVerificationService : SessionVerificationService {
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown) private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial) private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var _canVerifySessionFlow = MutableStateFlow(true) private var _canVerifySessionFlow = MutableStateFlow(true)
private var emojiList = emptyList<VerificationEmoji>() private var emojiList = persistentListOf<VerificationEmoji>()
var shouldFail = false var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState> =_verificationFlowState override val verificationFlowState: StateFlow<VerificationFlowState> =_verificationFlowState
@ -87,7 +89,7 @@ class FakeSessionVerificationService : SessionVerificationService {
} }
fun givenEmojiList(emojis: List<VerificationEmoji>) { fun givenEmojiList(emojis: List<VerificationEmoji>) {
this.emojiList = emojis this.emojiList = emojis.toPersistentList()
} }
override suspend fun reset() { override suspend fun reset() {

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.textcomposer
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.theme.bgSubtleTertiary import io.element.android.libraries.designsystem.theme.bgSubtleTertiary
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.wysiwyg.compose.RichTextEditorDefaults import io.element.android.wysiwyg.compose.RichTextEditorDefaults
@ -39,7 +40,8 @@ internal object ElementRichTextEditorStyle {
m3colors.primary m3colors.primary
} else { } else {
m3colors.secondary m3colors.secondary
} },
lineHeight = 16.25.sp,
), ),
cursor = RichTextEditorDefaults.cursorStyle( cursor = RichTextEditorDefaults.cursorStyle(
color = colors.iconAccentTertiary, color = colors.iconAccentTertiary,

View file

@ -44,7 +44,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -85,7 +84,6 @@ import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.wysiwyg.compose.PillStyle
import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditor
import io.element.android.wysiwyg.compose.RichTextEditorState import io.element.android.wysiwyg.compose.RichTextEditorState
import io.element.android.wysiwyg.display.TextDisplay import io.element.android.wysiwyg.display.TextDisplay
@ -444,8 +442,6 @@ private fun TextInput(
.fillMaxWidth(), .fillMaxWidth(),
style = ElementRichTextEditorStyle.create( style = ElementRichTextEditorStyle.create(
hasFocus = state.hasFocus hasFocus = state.hasFocus
).copy(
pill = PillStyle(Color.Red)
), ),
resolveMentionDisplay = resolveMentionDisplay, resolveMentionDisplay = resolveMentionDisplay,
resolveRoomMentionDisplay = resolveRoomMentionDisplay, resolveRoomMentionDisplay = resolveRoomMentionDisplay,

View file

@ -34,6 +34,7 @@ dependencies {
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
api(projects.libraries.usersearch.api) api(projects.libraries.usersearch.api)
implementation(libs.kotlinx.collections.immutable)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)

View file

@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -37,7 +38,7 @@ internal class MatrixUserListDataSourceTest {
searchTerm = "test", searchTerm = "test",
result = Result.success( result = Result.success(
MatrixSearchUserResults( MatrixSearchUserResults(
results = listOf( results = persistentListOf(
aMatrixUserProfile(), aMatrixUserProfile(),
aMatrixUserProfile(userId = A_USER_ID_2) aMatrixUserProfile(userId = A_USER_ID_2)
), ),

View file

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

View file

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