Merge branch 'develop' into feature/valere/message_shields

This commit is contained in:
Benoit Marty 2024-08-14 12:37:31 +02:00 committed by GitHub
commit 5d10b1fe85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
342 changed files with 5475 additions and 1377 deletions

View file

@ -106,6 +106,11 @@ interface MatrixRoom : Closeable {
*/
suspend fun timelineFocusedOnEvent(eventId: EventId): Result<Timeline>
/**
* Create a new timeline for the pinned events of the room.
*/
suspend fun pinnedEventsTimeline(): Result<Timeline>
fun destroy()
suspend fun subscribeToSync()
@ -180,6 +185,8 @@ interface MatrixRoom : Closeable {
suspend fun canUserTriggerRoomNotification(userId: UserId): Result<Boolean>
suspend fun canUserPinUnpin(userId: UserId): Result<Boolean>
suspend fun canUserJoinCall(userId: UserId): Result<Boolean> =
canUserSendState(userId, StateEventType.CALL_MEMBER)

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
@ -52,4 +53,5 @@ data class MatrixRoomInfo(
val hasRoomCall: Boolean,
val activeRoomCallParticipants: ImmutableList<String>,
val heroes: ImmutableList<MatrixUser>,
val pinnedEventIds: ImmutableList<EventId>
)

View file

@ -65,3 +65,8 @@ suspend fun MatrixRoom.canRedactOwn(): Result<Boolean> = canUserRedactOwn(sessio
* Shortcut for calling [MatrixRoom.canRedactOther] with our own user.
*/
suspend fun MatrixRoom.canRedactOther(): Result<Boolean> = canUserRedactOther(sessionId)
/**
* Shortcut for calling [MatrixRoom.canUserPinUnpin] with our own user.
*/
suspend fun MatrixRoom.canPinUnpin(): Result<Boolean> = canUserPinUnpin(sessionId)

View file

@ -169,4 +169,22 @@ interface Timeline : AutoCloseable {
): Result<MediaUploadHandler>
suspend fun loadReplyDetails(eventId: EventId): InReplyTo
/**
* Adds a new pinned event by sending an updated `m.room.pinned_events`
* event containing the new event id.
*
* Returns `true` if we sent the request, `false` if the event was already
* pinned.
*/
suspend fun pinEvent(eventId: EventId): Result<Boolean>
/**
* Adds a new pinned event by sending an updated `m.room.pinned_events`
* event without the event id we want to remove.
*
* Returns `true` if we sent the request, `false` if the event wasn't
* pinned
*/
suspend fun unpinEvent(eventId: EventId): Result<Boolean>
}

View file

@ -26,6 +26,7 @@ data class EventTimelineItem(
val eventId: EventId?,
val transactionId: TransactionId?,
val isEditable: Boolean,
val canBeRepliedTo: Boolean,
val isLocal: Boolean,
val isOwn: Boolean,
val isRemote: Boolean,

View file

@ -32,7 +32,14 @@ sealed interface OtherState {
data object RoomHistoryVisibility : OtherState
data object RoomJoinRules : OtherState
data class RoomName(val name: String?) : OtherState
data object RoomPinnedEvents : OtherState
data class RoomPinnedEvents(val change: Change) : OtherState {
enum class Change {
ADDED,
REMOVED,
CHANGED
}
}
data class RoomUserPowerLevels(val users: Map<String, Long>) : OtherState
data object RoomServerAcl : OtherState
data class RoomThirdPartyInvite(val displayName: String?) : OtherState

View file

@ -37,12 +37,13 @@ dependencies {
debugImplementation(libs.matrix.sdk)
}
implementation(projects.appconfig)
implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.di)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.network)
implementation(projects.libraries.preferences.api)
implementation(projects.services.analytics.api)
implementation(projects.services.toolbox.api)
implementation(projects.libraries.featureflag.api)
api(projects.libraries.matrix.api)
implementation(libs.dagger)
implementation(projects.libraries.core)

View file

@ -23,10 +23,12 @@ import io.element.android.libraries.matrix.impl.certificates.UserCertificatesPro
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.Session
@ -46,9 +48,18 @@ class RustMatrixClientFactory @Inject constructor(
private val proxyProvider: ProxyProvider,
private val clock: SystemClock,
private val utdTracker: UtdTracker,
private val appPreferencesStore: AppPreferencesStore,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val client = getBaseClientBuilder(sessionData.sessionPath, sessionData.passphrase)
val client = getBaseClientBuilder(
sessionPath = sessionData.sessionPath,
passphrase = sessionData.passphrase,
slidingSync = if (appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()) {
ClientBuilderSlidingSync.Simplified
} else {
ClientBuilderSlidingSync.Restored
},
)
.homeserverUrl(sessionData.homeserverUrl)
.username(sessionData.userId)
.use { it.build() }
@ -79,6 +90,7 @@ class RustMatrixClientFactory @Inject constructor(
sessionPath: String,
passphrase: String?,
slidingSyncProxy: String? = null,
slidingSync: ClientBuilderSlidingSync,
): ClientBuilder {
return ClientBuilder()
.sessionPath(sessionPath)
@ -88,6 +100,13 @@ class RustMatrixClientFactory @Inject constructor(
.addRootCertificates(userCertificatesProvider.provides())
.autoEnableBackups(true)
.autoEnableCrossSigning(true)
.run {
when (slidingSync) {
ClientBuilderSlidingSync.Restored -> this
ClientBuilderSlidingSync.Discovered -> requiresSlidingSync()
ClientBuilderSlidingSync.Simplified -> simplifiedSlidingSync(true)
}
}
.run {
// Workaround for non-nullable proxy parameter in the SDK, since each call to the ClientBuilder returns a new reference we need to keep
proxyProvider.provides()?.let { proxy(it) } ?: this
@ -95,6 +114,17 @@ class RustMatrixClientFactory @Inject constructor(
}
}
enum class ClientBuilderSlidingSync {
// The proxy will be supplied when restoring the Session.
Restored,
// A proxy must be discovered whilst building the session.
Discovered,
// Use Simplified Sliding Sync (discovery isn't a thing yet).
Simplified,
}
private fun SessionData.toSession() = Session(
accessToken = accessToken,
refreshToken = refreshToken,

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
@ -59,7 +60,7 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class RustMatrixAuthenticationService @Inject constructor(
baseDirectory: File,
private val baseDirectory: File,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
private val rustMatrixClientFactory: RustMatrixClientFactory,
@ -69,10 +70,19 @@ class RustMatrixAuthenticationService @Inject constructor(
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
// stored in the SessionData.
private val pendingPassphrase = getDatabasePassphrase()
private val sessionPath = File(baseDirectory, UUID.randomUUID().toString()).absolutePath
// Need to keep a copy of the current session path to eventually delete it.
// Ideally it would be possible to get the sessionPath from the Client to avoid doing this.
private var sessionPath: File? = null
private var currentClient: Client? = null
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private fun rotateSessionPath(): File {
sessionPath?.deleteRecursively()
return File(baseDirectory, UUID.randomUUID().toString())
.also { sessionPath = it }
}
override fun loggedInStateFlow(): Flow<LoggedInState> {
return sessionStore.isLoggedIn()
}
@ -116,8 +126,9 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun setHomeserver(homeserver: String): Result<Unit> =
withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath()
runCatching {
val client = getBaseClientBuilder()
val client = getBaseClientBuilder(emptySessionPath)
.serverNameOrHomeserverUrl(homeserver)
.build()
currentClient = client
@ -134,13 +145,14 @@ class RustMatrixAuthenticationService @Inject constructor(
withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPath = sessionPath ?: error("You need to call `setHomeserver()` first")
client.login(username, password, "Element X Android", null)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
sessionPath = currentSessionPath.absolutePath,
)
clear()
sessionStore.storeData(sessionData)
@ -184,13 +196,14 @@ class RustMatrixAuthenticationService @Inject constructor(
return withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPath = sessionPath ?: error("You need to call `setHomeserver()` first")
val urlForOidcLogin = pendingOidcAuthorizationData ?: error("You need to call `getOidcUrl()` first")
client.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
val sessionData = client.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
sessionPath = currentSessionPath.absolutePath,
)
clear()
pendingOidcAuthorizationData?.close()
@ -205,11 +218,13 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath()
runCatching {
val client = rustMatrixClientFactory.getBaseClientBuilder(
sessionPath = sessionPath,
sessionPath = emptySessionPath.absolutePath,
passphrase = pendingPassphrase,
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
slidingSync = ClientBuilderSlidingSync.Discovered,
)
.buildWithQrCode(
qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData,
@ -227,7 +242,7 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.QR,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
sessionPath = emptySessionPath.absolutePath,
)
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
@ -244,15 +259,17 @@ class RustMatrixAuthenticationService @Inject constructor(
}
Timber.e(throwable, "Failed to login with QR code")
}
}
}
private fun getBaseClientBuilder() = rustMatrixClientFactory
private fun getBaseClientBuilder(
sessionPath: File,
) = rustMatrixClientFactory
.getBaseClientBuilder(
sessionPath = sessionPath,
sessionPath = sessionPath.absolutePath,
passphrase = pendingPassphrase,
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
slidingSync = ClientBuilderSlidingSync.Discovered,
)
.requiresSlidingSync()
private fun clear() {
currentClient?.close()

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
@ -58,7 +59,8 @@ class MatrixRoomInfoMapper {
userDefinedNotificationMode = it.userDefinedNotificationMode?.map(),
hasRoomCall = it.hasRoomCall,
activeRoomCallParticipants = it.activeRoomCallParticipants.toImmutableList(),
heroes = it.elementHeroes().toImmutableList()
heroes = it.elementHeroes().toImmutableList(),
pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(),
)
}
}

View file

@ -51,23 +51,15 @@ class RoomSyncSubscriber(
includeHeroes = false,
)
suspend fun subscribe(roomId: RoomId) = mutex.withLock {
withContext(dispatchers.io) {
try {
subscribeToRoom(roomId)
} catch (exception: Exception) {
Timber.e("Failed to subscribe to room $roomId")
}
}
}
suspend fun batchSubscribe(roomIds: List<RoomId>) = mutex.withLock {
withContext(dispatchers.io) {
for (roomId in roomIds) {
suspend fun subscribe(roomId: RoomId) {
mutex.withLock {
withContext(dispatchers.io) {
try {
subscribeToRoom(roomId)
} catch (cancellationException: CancellationException) {
throw cancellationException
if (!isSubscribedTo(roomId)) {
Timber.d("Subscribing to room $roomId}")
roomListService.subscribeToRooms(listOf(roomId.value), settings)
}
subscribedRoomIds.add(roomId)
} catch (exception: Exception) {
Timber.e("Failed to subscribe to room $roomId")
}
@ -75,14 +67,21 @@ class RoomSyncSubscriber(
}
}
private fun subscribeToRoom(roomId: RoomId) {
if (!isSubscribedTo(roomId)) {
Timber.d("Subscribing to room $roomId}")
roomListService.room(roomId.value).use { roomListItem ->
roomListItem.subscribe(settings)
suspend fun batchSubscribe(roomIds: List<RoomId>) = mutex.withLock {
withContext(dispatchers.io) {
try {
val roomIdsToSubscribeTo = roomIds.filterNot { isSubscribedTo(it) }
if (roomIdsToSubscribeTo.isNotEmpty()) {
Timber.d("Subscribing to rooms: $roomIds")
roomListService.subscribeToRooms(roomIdsToSubscribeTo.map { it.value }, settings)
subscribedRoomIds.addAll(roomIds)
}
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (exception: Exception) {
Timber.e(exception, "Failed to subscribe to rooms: $roomIds")
}
}
subscribedRoomIds.add(roomId)
}
fun isSubscribedTo(roomId: RoomId): Boolean {

View file

@ -192,6 +192,21 @@ class RustMatrixRoom(
}
}
override suspend fun pinnedEventsTimeline(): Result<Timeline> {
return runCatching {
innerRoom.pinnedEventsTimeline(
internalIdPrefix = "pinned_events",
maxEventsToLoad = 100u,
).let { inner ->
createTimeline(inner, isLive = false)
}
}.onFailure {
if (it is CancellationException) {
throw it
}
}
}
override fun destroy() {
roomCoroutineScope.cancel()
liveTimeline.close()
@ -403,6 +418,12 @@ class RustMatrixRoom(
}
}
override suspend fun canUserPinUnpin(userId: UserId): Result<Boolean> {
return runCatching {
innerRoom.canUserPinUnpin(userId.value)
}
}
override suspend fun sendImage(
file: File,
thumbnailFile: File?,

View file

@ -525,6 +525,18 @@ class RustTimeline(
}
}
override suspend fun pinEvent(eventId: EventId): Result<Boolean> = withContext(dispatcher) {
runCatching {
inner.pinEvent(eventId = eventId.value)
}
}
override suspend fun unpinEvent(eventId: EventId): Result<Boolean> = withContext(dispatcher) {
runCatching {
inner.unpinEvent(eventId = eventId.value)
}
}
private suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> {
return runCatching {
inner.fetchDetailsForEvent(eventId.value)

View file

@ -47,6 +47,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
eventId = it.eventId()?.let(::EventId),
transactionId = it.transactionId()?.let(::TransactionId),
isEditable = it.isEditable(),
canBeRepliedTo = it.canBeRepliedTo(),
isLocal = it.isLocal(),
isOwn = it.isOwn(),
isRemote = it.isRemote(),

View file

@ -40,6 +40,7 @@ import kotlinx.collections.immutable.toImmutableMap
import org.matrix.rustcomponents.sdk.TimelineItemContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import org.matrix.rustcomponents.sdk.use
import uniffi.matrix_sdk_ui.RoomPinnedEventsChange
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
import org.matrix.rustcomponents.sdk.MembershipChange as RustMembershipChange
import org.matrix.rustcomponents.sdk.OtherState as RustOtherState
@ -176,7 +177,7 @@ private fun RustOtherState.map(): OtherState {
RustOtherState.RoomHistoryVisibility -> OtherState.RoomHistoryVisibility
RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules
is RustOtherState.RoomName -> OtherState.RoomName(name)
RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents
is RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents(change.map())
is RustOtherState.RoomPowerLevels -> OtherState.RoomUserPowerLevels(users)
RustOtherState.RoomServerAcl -> OtherState.RoomServerAcl
is RustOtherState.RoomThirdPartyInvite -> OtherState.RoomThirdPartyInvite(displayName)
@ -187,6 +188,14 @@ private fun RustOtherState.map(): OtherState {
}
}
private fun RoomPinnedEventsChange.map(): OtherState.RoomPinnedEvents.Change {
return when (this) {
RoomPinnedEventsChange.ADDED -> OtherState.RoomPinnedEvents.Change.ADDED
RoomPinnedEventsChange.REMOVED -> OtherState.RoomPinnedEvents.Change.REMOVED
RoomPinnedEventsChange.CHANGED -> OtherState.RoomPinnedEvents.Change.CHANGED
}
}
private fun RustEncryptedMessage.map(): UnableToDecryptContent.Data {
return when (this) {
is RustEncryptedMessage.MegolmV1AesSha2 -> UnableToDecryptContent.Data.MegolmV1AesSha2(sessionId, cause.map())

View file

@ -189,6 +189,8 @@ class RoomSummaryListProcessorTest {
override fun syncIndicator(delayBeforeShowingInMs: UInt, delayBeforeHidingInMs: UInt, listener: RoomListServiceSyncIndicatorListener): TaskHandle {
return TaskHandle(Pointer.NULL)
}
override fun subscribeToRooms(roomIds: List<String>, settings: RoomSubscription?) = Unit
}
}
@ -221,6 +223,7 @@ private fun aRustRoomInfo(
numUnreadMessages: ULong = 0uL,
numUnreadNotifications: ULong = 0uL,
numUnreadMentions: ULong = 0uL,
pinnedEventIds: List<String> = listOf(),
) = RoomInfo(
id = id,
displayName = displayName,
@ -249,7 +252,8 @@ private fun aRustRoomInfo(
isMarkedUnread = isMarkedUnread,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
numUnreadMentions = numUnreadMentions
numUnreadMentions = numUnreadMentions,
pinnedEventIds = pinnedEventIds,
)
class FakeRoomListItem(
@ -268,6 +272,4 @@ class FakeRoomListItem(
override suspend fun latestEvent(): EventTimelineItem? {
return latestEvent
}
override fun subscribe(settings: RoomSubscription?) = Unit
}

View file

@ -125,6 +125,7 @@ class FakeMatrixRoom(
private val getWidgetDriverResult: (MatrixWidgetSettings) -> Result<MatrixWidgetDriver> = { lambdaError() },
private val canUserTriggerRoomNotificationResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canUserJoinCallResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val canUserPinUnpinResult: (UserId) -> Result<Boolean> = { lambdaError() },
private val setIsFavoriteResult: (Boolean) -> Result<Unit> = { lambdaError() },
private val powerLevelsResult: () -> Result<MatrixRoomPowerLevels> = { lambdaError() },
private val updatePowerLevelsResult: () -> Result<Unit> = { lambdaError() },
@ -134,10 +135,12 @@ class FakeMatrixRoom(
private val updateMembersResult: () -> Unit = { lambdaError() },
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
private val timelineFocusedOnEventResult: (EventId) -> Result<Timeline> = { lambdaError() },
private val pinnedEventsTimelineResult: () -> Result<Timeline> = { lambdaError() },
private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> },
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
private val clearComposerDraftLambda: () -> Result<Unit> = { Result.success(Unit) },
private val subscribeToSyncLambda: () -> Unit = { lambdaError() },
) : MatrixRoom {
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
@ -180,7 +183,13 @@ class FakeMatrixRoom(
timelineFocusedOnEventResult(eventId)
}
override suspend fun subscribeToSync() = Unit
override suspend fun pinnedEventsTimeline(): Result<Timeline> = simulateLongTask {
pinnedEventsTimelineResult()
}
override suspend fun subscribeToSync() {
subscribeToSyncLambda()
}
override suspend fun powerLevels(): Result<MatrixRoomPowerLevels> {
return powerLevelsResult()
@ -289,6 +298,10 @@ class FakeMatrixRoom(
return canUserJoinCallResult(userId)
}
override suspend fun canUserPinUnpin(userId: UserId): Result<Boolean> {
return canUserPinUnpinResult(userId)
}
override suspend fun sendImage(
file: File,
thumbnailFile: File?,
@ -517,6 +530,7 @@ fun aRoomInfo(
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<String> = emptyList(),
heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(),
) = MatrixRoomInfo(
id = id,
name = name,
@ -542,6 +556,7 @@ fun aRoomInfo(
userPowerLevels = userPowerLevels,
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
heroes = heroes.toImmutableList(),
pinnedEventIds = pinnedEventIds.toImmutableList(),
)
fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(

View file

@ -22,22 +22,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeSyncService(
initialState: SyncState = SyncState.Idle
syncStateFlow: MutableStateFlow<SyncState> = MutableStateFlow(SyncState.Idle)
) : SyncService {
private val syncStateFlow = MutableStateFlow(initialState)
fun simulateError() {
syncStateFlow.value = SyncState.Error
}
var startSyncLambda: () -> Result<Unit> = { Result.success(Unit) }
override suspend fun startSync(): Result<Unit> {
syncStateFlow.value = SyncState.Running
return Result.success(Unit)
return startSyncLambda()
}
var stopSyncLambda: () -> Result<Unit> = { Result.success(Unit) }
override suspend fun stopSync(): Result<Unit> {
syncStateFlow.value = SyncState.Terminated
return Result.success(Unit)
return stopSyncLambda()
}
override val syncState: StateFlow<SyncState> = syncStateFlow

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -371,6 +372,16 @@ class FakeTimeline(
override suspend fun loadReplyDetails(eventId: EventId) = loadReplyDetailsLambda(eventId)
var pinEventLambda: (eventId: EventId) -> Result<Boolean> = { lambdaError() }
override suspend fun pinEvent(eventId: EventId): Result<Boolean> {
return pinEventLambda(eventId)
}
var unpinEventLambda: (eventId: EventId) -> Result<Boolean> = { lambdaError() }
override suspend fun unpinEvent(eventId: EventId): Result<Boolean> {
return unpinEventLambda(eventId)
}
var closeCounter = 0
private set

View file

@ -47,6 +47,7 @@ fun anEventTimelineItem(
eventId: EventId = AN_EVENT_ID,
transactionId: TransactionId? = null,
isEditable: Boolean = false,
canBeRepliedTo: Boolean = false,
isLocal: Boolean = false,
isOwn: Boolean = false,
isRemote: Boolean = false,
@ -63,6 +64,7 @@ fun anEventTimelineItem(
eventId = eventId,
transactionId = transactionId,
isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
isLocal = isLocal,
isOwn = isOwn,
isRemote = isRemote,