Merge branch 'develop' of https://github.com/vector-im/element-x-android into dla/feature/custom_room_notification_settings_list
This commit is contained in:
commit
87b8bfe99d
1981 changed files with 15504 additions and 5063 deletions
|
|
@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.impl.notificationsettings.RustNotific
|
|||
import io.element.android.libraries.matrix.impl.oidc.toRustAction
|
||||
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
|
||||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
||||
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
|
||||
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
|
||||
|
|
@ -114,6 +115,7 @@ class RustMatrixClient constructor(
|
|||
|
||||
private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
|
||||
private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers)
|
||||
private val roomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers)
|
||||
|
||||
private val isLoggingOut = AtomicBoolean(false)
|
||||
|
||||
|
|
@ -124,7 +126,16 @@ class RustMatrixClient constructor(
|
|||
Timber.v("didReceiveAuthError -> do the cleanup")
|
||||
//TODO handle isSoftLogout parameter.
|
||||
appCoroutineScope.launch {
|
||||
doLogout(doRequest = false)
|
||||
val existingData = sessionStore.getSession(client.userId())
|
||||
if (existingData != null) {
|
||||
// Set isTokenValid to false
|
||||
val newData = client.session().toSessionData(
|
||||
isTokenValid = false,
|
||||
loginType = existingData.loginType,
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
}
|
||||
doLogout(doRequest = false, removeSession = false)
|
||||
}
|
||||
} else {
|
||||
Timber.v("didReceiveAuthError -> already cleaning up")
|
||||
|
|
@ -134,7 +145,12 @@ class RustMatrixClient constructor(
|
|||
override fun didRefreshTokens() {
|
||||
Timber.w("didRefreshTokens()")
|
||||
appCoroutineScope.launch {
|
||||
sessionStore.updateData(client.session().toSessionData())
|
||||
val existingData = sessionStore.getSession(client.userId()) ?: return@launch
|
||||
val newData = client.session().toSessionData(
|
||||
isTokenValid = existingData.isTokenValid,
|
||||
loginType = existingData.loginType,
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -185,13 +201,15 @@ class RustMatrixClient constructor(
|
|||
systemClock = clock,
|
||||
roomContentForwarder = roomContentForwarder,
|
||||
sessionData = sessionStore.getSession(sessionId.value)!!,
|
||||
roomSyncSubscriber = roomSyncSubscriber
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
|
||||
val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
|
||||
val fullRoom = cachedRoomListItem?.fullRoom()
|
||||
// Keep using fullRoomBlocking for now as it's faster.
|
||||
val fullRoom = cachedRoomListItem?.fullRoomBlocking()
|
||||
return if (cachedRoomListItem == null || fullRoom == null) {
|
||||
Timber.d("No room cached for $roomId")
|
||||
null
|
||||
|
|
@ -281,10 +299,9 @@ class RustMatrixClient constructor(
|
|||
runCatching { client.setDisplayName(displayName) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit> =
|
||||
withContext(sessionDispatcher) {
|
||||
runCatching { client.uploadAvatar(mimeType, data.toUByteArray().toList()) }
|
||||
runCatching { client.uploadAvatar(mimeType, data) }
|
||||
}
|
||||
|
||||
override suspend fun removeAvatar(): Result<Unit> =
|
||||
|
|
@ -292,7 +309,6 @@ class RustMatrixClient constructor(
|
|||
runCatching { client.removeAvatar() }
|
||||
}
|
||||
|
||||
|
||||
override fun syncService(): SyncService = rustSyncService
|
||||
|
||||
override fun sessionVerificationService(): SessionVerificationService = verificationService
|
||||
|
|
@ -326,9 +342,9 @@ class RustMatrixClient constructor(
|
|||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
|
||||
}
|
||||
|
||||
override suspend fun logout(): String? = doLogout(doRequest = true)
|
||||
override suspend fun logout(): String? = doLogout(doRequest = true, removeSession = true)
|
||||
|
||||
private suspend fun doLogout(doRequest: Boolean): String? {
|
||||
private suspend fun doLogout(doRequest: Boolean, removeSession: Boolean): String? {
|
||||
var result: String? = null
|
||||
withContext(sessionDispatcher) {
|
||||
if (doRequest) {
|
||||
|
|
@ -340,7 +356,9 @@ class RustMatrixClient constructor(
|
|||
}
|
||||
close()
|
||||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
|
||||
sessionStore.removeSession(sessionId.value)
|
||||
if (removeSession) {
|
||||
sessionStore.removeSession(sessionId.value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -364,10 +382,9 @@ class RustMatrixClient constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
client.uploadMedia(mimeType, data.toUByteArray().toList(), progressCallback?.toProgressWatcher())
|
||||
client.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,18 +20,19 @@ import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
|||
import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException
|
||||
|
||||
fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||
val message = this.message ?: "Unknown error"
|
||||
return when (this) {
|
||||
is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(this.message!!)
|
||||
is RustAuthenticationException.Generic -> AuthenticationException.Generic(this.message!!)
|
||||
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
|
||||
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
|
||||
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
|
||||
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
|
||||
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
|
||||
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
|
||||
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
|
||||
is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message!!)
|
||||
is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message!!)
|
||||
else -> AuthenticationException.Generic(this.message ?: "Unknown error")
|
||||
is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(message)
|
||||
is RustAuthenticationException.Generic -> AuthenticationException.Generic(message)
|
||||
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
|
||||
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(message)
|
||||
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(message)
|
||||
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message)
|
||||
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message)
|
||||
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message)
|
||||
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message)
|
||||
is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message)
|
||||
is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message)
|
||||
else -> AuthenticationException.Generic(message)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
|||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -62,7 +64,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
)
|
||||
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
|
||||
override fun isLoggedIn(): Flow<Boolean> {
|
||||
override fun loggedInStateFlow(): Flow<LoggedInState> {
|
||||
return sessionStore.isLoggedIn()
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +76,11 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
runCatching {
|
||||
val sessionData = sessionStore.getSession(sessionId.value)
|
||||
if (sessionData != null) {
|
||||
rustMatrixClientFactory.create(sessionData)
|
||||
if (sessionData.isTokenValid) {
|
||||
rustMatrixClientFactory.create(sessionData)
|
||||
} else {
|
||||
error("Token is not valid")
|
||||
}
|
||||
} else {
|
||||
error("No session to restore with id $sessionId")
|
||||
}
|
||||
|
|
@ -102,7 +108,12 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val client = authService.login(username, password, "Element X Android", null)
|
||||
val sessionData = client.use { it.session().toSessionData() }
|
||||
val sessionData = client.use {
|
||||
it.session().toSessionData(
|
||||
isTokenValid = true,
|
||||
loginType = LoginType.PASSWORD,
|
||||
)
|
||||
}
|
||||
sessionStore.storeData(sessionData)
|
||||
SessionId(sessionData.userId)
|
||||
}.mapFailure { failure ->
|
||||
|
|
@ -144,7 +155,12 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
runCatching {
|
||||
val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first")
|
||||
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
|
||||
val sessionData = client.use { it.session().toSessionData() }
|
||||
val sessionData = client.use {
|
||||
it.session().toSessionData(
|
||||
isTokenValid = true,
|
||||
loginType = LoginType.OIDC,
|
||||
)
|
||||
}
|
||||
pendingOidcAuthenticationData?.close()
|
||||
pendingOidcAuthenticationData = null
|
||||
sessionStore.storeData(sessionData)
|
||||
|
|
|
|||
|
|
@ -16,11 +16,15 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
import java.util.Date
|
||||
|
||||
internal fun Session.toSessionData() = SessionData(
|
||||
internal fun Session.toSessionData(
|
||||
isTokenValid: Boolean,
|
||||
loginType: LoginType,
|
||||
) = SessionData(
|
||||
userId = userId,
|
||||
deviceId = deviceId,
|
||||
accessToken = accessToken,
|
||||
|
|
@ -29,4 +33,6 @@ internal fun Session.toSessionData() = SessionData(
|
|||
oidcData = oidcData,
|
||||
slidingSyncProxy = slidingSyncProxy,
|
||||
loginTimestamp = Date(),
|
||||
isTokenValid = isTokenValid,
|
||||
loginType = loginType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.media
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
|
|
@ -77,7 +78,7 @@ class RustMediaLoader(
|
|||
val mediaFile = innerClient.getMediaFile(
|
||||
mediaSource = mediaSource,
|
||||
body = body,
|
||||
mimeType = mimeType ?: "application/octet-stream",
|
||||
mimeType = mimeType ?: MimeTypes.OctetStream,
|
||||
tempDir = cacheDirectory.path,
|
||||
)
|
||||
RustMediaFile(mediaFile)
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
|
|||
}
|
||||
MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction
|
||||
MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker
|
||||
is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(senderId, question)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import org.matrix.rustcomponents.sdk.Room
|
|||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
|
|
@ -61,7 +60,7 @@ class RoomContentForwarder(
|
|||
// Sending a message requires a registered timeline listener
|
||||
targetRoom.addTimelineListener(NoOpTimelineListener)
|
||||
withTimeout(timeoutMs.milliseconds) {
|
||||
targetRoom.send(content, genTransactionId())
|
||||
targetRoom.send(content)
|
||||
}
|
||||
}
|
||||
// After sending, we remove the timeline
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.RequiredState
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.RoomSubscription
|
||||
import timber.log.Timber
|
||||
|
||||
class RoomSyncSubscriber(
|
||||
private val roomListService: RoomListService,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
|
||||
private val subscriptionCounts = HashMap<RoomId, Int>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
private val settings = RoomSubscription(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
|
||||
),
|
||||
timelineLimit = null
|
||||
)
|
||||
|
||||
suspend fun subscribe(roomId: RoomId) = mutex.withLock {
|
||||
withContext(dispatchers.io) {
|
||||
try {
|
||||
val currentSubscription = subscriptionCounts.getOrElse(roomId) { 0 }
|
||||
if (currentSubscription == 0) {
|
||||
Timber.d("Subscribing to room $roomId}")
|
||||
roomListService.room(roomId.value).use { roomListItem ->
|
||||
roomListItem.subscribe(settings)
|
||||
}
|
||||
}
|
||||
subscriptionCounts[roomId] = currentSubscription + 1
|
||||
} catch (exception: Exception) {
|
||||
Timber.e("Failed to subscribe to room $roomId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unsubscribe(roomId: RoomId) = mutex.withLock {
|
||||
withContext(dispatchers.io) {
|
||||
try {
|
||||
val currentSubscription = subscriptionCounts.getOrElse(roomId) { 0 }
|
||||
when (currentSubscription) {
|
||||
0 -> return@withContext
|
||||
1 -> {
|
||||
Timber.d("Unsubscribe from room $roomId")
|
||||
roomListService.room(roomId.value).use { roomListItem ->
|
||||
roomListItem.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
subscriptionCounts[roomId] = currentSubscription - 1
|
||||
} catch (exception: Exception) {
|
||||
Timber.e("Failed to unsubscribe from room $roomId")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,6 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
|
|||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
||||
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
|
|
@ -55,19 +54,17 @@ import kotlinx.coroutines.CancellationException
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import org.matrix.rustcomponents.sdk.RequiredState
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomMember
|
||||
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
||||
import org.matrix.rustcomponents.sdk.RoomSubscription
|
||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
import timber.log.Timber
|
||||
|
|
@ -84,6 +81,7 @@ class RustMatrixRoom(
|
|||
private val systemClock: SystemClock,
|
||||
private val roomContentForwarder: RoomContentForwarder,
|
||||
private val sessionData: SessionData,
|
||||
private val roomSyncSubscriber: RoomSyncSubscriber,
|
||||
) : MatrixRoom {
|
||||
|
||||
override val roomId = RoomId(innerRoom.id())
|
||||
|
|
@ -118,27 +116,15 @@ class RustMatrixRoom(
|
|||
|
||||
override val timeline: MatrixTimeline = _timeline
|
||||
|
||||
override fun subscribeToSync() {
|
||||
val settings = RoomSubscription(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
|
||||
),
|
||||
timelineLimit = null
|
||||
)
|
||||
roomListItem.subscribe(settings)
|
||||
}
|
||||
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
|
||||
|
||||
override fun unsubscribeFromSync() {
|
||||
roomListItem.unsubscribe()
|
||||
}
|
||||
override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId)
|
||||
|
||||
override fun destroy() {
|
||||
roomCoroutineScope.cancel()
|
||||
innerRoom.destroy()
|
||||
roomListItem.destroy()
|
||||
specialModeEventTimelineItem?.destroy()
|
||||
}
|
||||
|
||||
override val name: String?
|
||||
|
|
@ -193,7 +179,7 @@ class RustMatrixRoom(
|
|||
while (true) {
|
||||
// Loading the whole membersIterator as a stop-gap measure.
|
||||
// We should probably implement some sort of paging in the future.
|
||||
yield()
|
||||
ensureActive()
|
||||
addAll(membersIterator.nextChunk(1000u) ?: break)
|
||||
}
|
||||
}
|
||||
|
|
@ -241,10 +227,9 @@ class RustMatrixRoom(
|
|||
}
|
||||
|
||||
override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
|
||||
val transactionId = genTransactionId()
|
||||
messageEventContentFromParts(body, htmlBody).use { content ->
|
||||
runCatching {
|
||||
innerRoom.send(content, transactionId)
|
||||
innerRoom.send(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -253,26 +238,46 @@ class RustMatrixRoom(
|
|||
withContext(roomDispatcher) {
|
||||
if (originalEventId != null) {
|
||||
runCatching {
|
||||
innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value)
|
||||
val editedEvent = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(originalEventId.value)
|
||||
editedEvent.use {
|
||||
innerRoom.edit(
|
||||
newContent = messageEventContentFromParts(body, htmlBody),
|
||||
editItem = it,
|
||||
)
|
||||
}
|
||||
specialModeEventTimelineItem = null
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
transactionId?.let { cancelSend(it) }
|
||||
innerRoom.send(messageEventContentFromParts(body, htmlBody), genTransactionId())
|
||||
innerRoom.send(messageEventContentFromParts(body, htmlBody))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var specialModeEventTimelineItem: EventTimelineItem? = null
|
||||
|
||||
override suspend fun enterSpecialMode(eventId: EventId?): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
specialModeEventTimelineItem?.destroy()
|
||||
specialModeEventTimelineItem = null
|
||||
specialModeEventTimelineItem = eventId?.let { innerRoom.getEventTimelineItemByEventId(it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventId.value, genTransactionId())
|
||||
val inReplyTo = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(eventId.value)
|
||||
inReplyTo.use { eventTimelineItem ->
|
||||
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventTimelineItem)
|
||||
}
|
||||
specialModeEventTimelineItem = null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) {
|
||||
val transactionId = genTransactionId()
|
||||
runCatching {
|
||||
innerRoom.redact(eventId.value, reason, transactionId)
|
||||
innerRoom.redact(eventId.value, reason)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,10 +373,9 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.uploadAvatar(mimeType, data.toUByteArray().toList())
|
||||
innerRoom.uploadAvatar(mimeType, data, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -416,7 +420,6 @@ class RustMatrixRoom(
|
|||
description = description,
|
||||
zoomLevel = zoomLevel?.toUByte(),
|
||||
assetType = assetType?.toInner(),
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -433,7 +436,6 @@ class RustMatrixRoom(
|
|||
answers = answers,
|
||||
maxSelections = maxSelections.toUByte(),
|
||||
pollKind = pollKind.toInner(),
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -446,7 +448,6 @@ class RustMatrixRoom(
|
|||
innerRoom.sendPollResponse(
|
||||
pollStartId = pollStartId.value,
|
||||
answers = answers,
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -459,11 +460,24 @@ class RustMatrixRoom(
|
|||
innerRoom.endPoll(
|
||||
pollStartId = pollStartId.value,
|
||||
text = text,
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendVoiceMessage(
|
||||
file: File,
|
||||
audioInfo: AudioInfo,
|
||||
waveform: List<Int>,
|
||||
progressCallback: ProgressCallback?,
|
||||
): Result<MediaUploadHandler> = sendAttachment(listOf(file)) {
|
||||
innerRoom.sendVoiceMessage(
|
||||
url = file.path,
|
||||
audioInfo = audioInfo.map(),
|
||||
waveform = waveform.map { it.toUShort() },
|
||||
progressWatcher = progressCallback?.toProgressWatcher(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
|
||||
return runCatching {
|
||||
MediaUploadHandlerImpl(files, handle())
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ import kotlinx.coroutines.channels.trySendBlocking
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import org.matrix.rustcomponents.sdk.RoomList
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||
import org.matrix.rustcomponents.sdk.RoomListInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceState
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
|
||||
|
|
@ -40,7 +40,7 @@ import timber.log.Timber
|
|||
private const val SYNC_INDICATOR_DELAY_BEFORE_SHOWING = 1000u
|
||||
private const val SYNC_INDICATOR_DELAY_BEFORE_HIDING = 0u
|
||||
|
||||
fun RoomList.loadingStateFlow(): Flow<RoomListLoadingState> =
|
||||
fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListLoadingStateListener {
|
||||
override fun onUpdate(state: RoomListLoadingState) {
|
||||
|
|
@ -58,7 +58,7 @@ fun RoomList.loadingStateFlow(): Flow<RoomListLoadingState> =
|
|||
Timber.d(it, "loadingStateFlow() failed")
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<List<RoomListEntriesUpdate>> =
|
||||
fun RoomListInterface.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<List<RoomListEntriesUpdate>> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListEntriesListener {
|
||||
override fun onUpdate(roomEntriesUpdate: List<RoomListEntriesUpdate>) {
|
||||
|
|
@ -76,7 +76,7 @@ fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit):
|
|||
Timber.d(it, "entriesFlow() failed")
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
fun RoomListService.stateFlow(): Flow<RoomListServiceState> =
|
||||
fun RoomListServiceInterface.stateFlow(): Flow<RoomListServiceState> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListServiceStateListener {
|
||||
override fun onUpdate(state: RoomListServiceState) {
|
||||
|
|
@ -88,7 +88,7 @@ fun RoomListService.stateFlow(): Flow<RoomListServiceState> =
|
|||
}
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
fun RoomListService.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
|
||||
fun RoomListServiceInterface.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListServiceSyncIndicatorListener {
|
||||
override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) {
|
||||
|
|
@ -104,7 +104,7 @@ fun RoomListService.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
|
|||
}
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
|
||||
fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? {
|
||||
return try {
|
||||
room(roomId)
|
||||
} catch (exception: Exception) {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ import kotlinx.coroutines.sync.withLock
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
class RoomSummaryListProcessor(
|
||||
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
|
||||
private val roomListService: RoomListService,
|
||||
private val roomListService: RoomListServiceInterface,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
) {
|
||||
|
|
@ -73,7 +73,7 @@ class RoomSummaryListProcessor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
|
||||
private suspend fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
|
||||
when (update) {
|
||||
is RoomListEntriesUpdate.Append -> {
|
||||
val roomSummaries = update.values.map {
|
||||
|
|
@ -119,7 +119,7 @@ class RoomSummaryListProcessor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
|
||||
private suspend fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
|
||||
return when (entry) {
|
||||
RoomListEntry.Empty -> buildEmptyRoomSummary()
|
||||
is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId)
|
||||
|
|
@ -133,9 +133,9 @@ class RoomSummaryListProcessor(
|
|||
return RoomSummary.Empty(UUID.randomUUID().toString())
|
||||
}
|
||||
|
||||
private fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
|
||||
private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
|
||||
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
|
||||
roomListItem.roomInfoBlocking().use { roomInfo ->
|
||||
roomListItem.roomInfo().use { roomInfo ->
|
||||
RoomSummary.Filled(
|
||||
details = roomSummaryDetailsFactory.create(roomInfo)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.auth
|
||||
|
||||
import com.google.common.truth.ThrowableSubject
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException
|
||||
|
||||
class AuthenticationExceptionMappingTest {
|
||||
|
||||
@Test
|
||||
fun `mapping an exception with no message returns 'Unknown error' message`() {
|
||||
val exception = Exception()
|
||||
val mappedException = exception.mapAuthenticationException()
|
||||
assertThat(mappedException.message).isEqualTo("Unknown error")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapping a generic exception returns a Generic AuthenticationException`() {
|
||||
val exception = Exception("Generic exception")
|
||||
val mappedException = exception.mapAuthenticationException()
|
||||
assertThat(mappedException).isException<AuthenticationException.Generic>("Generic exception")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapping specific exceptions map to their kotlin counterparts`() {
|
||||
assertThat(RustAuthenticationException.ClientMissing("Client missing").mapAuthenticationException())
|
||||
.isException<AuthenticationException.ClientMissing>("Client missing")
|
||||
|
||||
assertThat(RustAuthenticationException.Generic("Generic").mapAuthenticationException()).isException<AuthenticationException.Generic>("Generic")
|
||||
|
||||
assertThat(RustAuthenticationException.InvalidServerName("Invalid server name").mapAuthenticationException())
|
||||
.isException<AuthenticationException.InvalidServerName>("Invalid server name")
|
||||
|
||||
assertThat(RustAuthenticationException.SessionMissing("Session missing").mapAuthenticationException())
|
||||
.isException<AuthenticationException.SessionMissing>("Session missing")
|
||||
|
||||
assertThat(RustAuthenticationException.SlidingSyncNotAvailable("Sliding sync not available").mapAuthenticationException())
|
||||
.isException<AuthenticationException.SlidingSyncNotAvailable>("Sliding sync not available")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mapping Oidc related exceptions creates an 'OidcError' with different types`() {
|
||||
assertIsOidcError(
|
||||
throwable = RustAuthenticationException.OidcException("Oidc exception"),
|
||||
type = "OidcException",
|
||||
message = "Oidc exception"
|
||||
)
|
||||
assertIsOidcError(
|
||||
throwable = RustAuthenticationException.OidcMetadataInvalid("Oidc metadata invalid"),
|
||||
type = "OidcMetadataInvalid",
|
||||
message = "Oidc metadata invalid"
|
||||
)
|
||||
assertIsOidcError(
|
||||
throwable = RustAuthenticationException.OidcMetadataMissing("Oidc metadata missing"),
|
||||
type = "OidcMetadataMissing",
|
||||
message = "Oidc metadata missing"
|
||||
)
|
||||
assertIsOidcError(
|
||||
throwable = RustAuthenticationException.OidcNotSupported("Oidc not supported"),
|
||||
type = "OidcNotSupported",
|
||||
message = "Oidc not supported"
|
||||
)
|
||||
assertIsOidcError(
|
||||
throwable = RustAuthenticationException.OidcCancelled("Oidc cancelled"),
|
||||
type = "OidcCancelled",
|
||||
message = "Oidc cancelled"
|
||||
)
|
||||
assertIsOidcError(
|
||||
throwable = RustAuthenticationException.OidcCallbackUrlInvalid("Oidc callback url invalid"),
|
||||
type = "OidcCallbackUrlInvalid",
|
||||
message = "Oidc callback url invalid"
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified T> ThrowableSubject.isException(message: String) {
|
||||
isInstanceOf(T::class.java)
|
||||
hasMessageThat().isEqualTo(message)
|
||||
}
|
||||
|
||||
private fun assertIsOidcError(throwable: Throwable, type: String, message: String) {
|
||||
val authenticationException = throwable.mapAuthenticationException()
|
||||
assertThat(authenticationException).isInstanceOf(AuthenticationException.OidcError::class.java)
|
||||
assertThat((authenticationException as? AuthenticationException.OidcError)?.type).isEqualTo(type)
|
||||
assertThat(authenticationException.message).isEqualTo(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.roomlist
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.sun.jna.Pointer
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.RoomList
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||
import org.matrix.rustcomponents.sdk.RoomListInput
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
// NOTE: this class is using a fake implementation of a Rust SDK interface which returns actual Rust objects with pointers.
|
||||
// Since we don't access the data in those objects, this is fine for our tests, but that's as far as we can test this class.
|
||||
class RoomSummaryListProcessorTests {
|
||||
|
||||
private val summaries = MutableStateFlow<List<RoomSummary>>(emptyList())
|
||||
|
||||
@Test
|
||||
fun `postUpdates can't start until postEntries is done`() = runTest {
|
||||
val processor = createProcessor()
|
||||
val update = listOf(RoomListEntriesUpdate.Reset(emptyList()))
|
||||
|
||||
val timeoutError = runCatching {
|
||||
withTimeout(10.milliseconds) { processor.postUpdate(update) }
|
||||
}.exceptionOrNull()
|
||||
assertThat(timeoutError).isInstanceOf(CancellationException::class.java)
|
||||
|
||||
processor.postEntries(listOf(RoomListEntry.Empty))
|
||||
processor.postUpdate(update)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `postEntries adds all new entries with no diffing`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
|
||||
processor.postEntries(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Append adds new entries at the end of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty))))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(4)
|
||||
assertThat(summaries.value.subList(1, 4).all { it is RoomSummary.Empty }).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PushBack adds a new entry at the end of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(RoomListEntry.Empty)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(2)
|
||||
assertThat(summaries.value.last()).isInstanceOf(RoomSummary.Empty::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PushFront inserts a new entry at the start of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(RoomListEntry.Empty)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(2)
|
||||
assertThat(summaries.value.first()).isInstanceOf(RoomSummary.Empty::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Set replaces an entry at some index`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), RoomListEntry.Empty)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
assertThat(summaries.value[index]).isInstanceOf(RoomSummary.Empty::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Insert inserts a new entry at the provided index`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), RoomListEntry.Empty)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(2)
|
||||
assertThat(summaries.value[index]).isInstanceOf(RoomSummary.Empty::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Remove removes an entry at some index`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Remove(index.toUInt())))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID_2.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PopBack removes an entry at the end of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PopBack))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PopFront removes an entry at the start of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PopFront))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID_2.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Clear removes all the entries`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
|
||||
val processor = createProcessor()
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
|
||||
|
||||
assertThat(summaries.value).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Truncate removes all entries after the provided length`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
// Start processing updates
|
||||
processor.postEntries(listOf())
|
||||
// Process actual update
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Truncate(1u)))
|
||||
|
||||
assertThat(summaries.value.count()).isEqualTo(1)
|
||||
assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID.value)
|
||||
}
|
||||
|
||||
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
|
||||
summaries,
|
||||
fakeRoomListService,
|
||||
dispatcher = StandardTestDispatcher(testScheduler),
|
||||
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
)
|
||||
|
||||
// Fake room list service that returns Rust objects with null pointers. Luckily for us, they don't crash for our test cases
|
||||
private val fakeRoomListService = object : RoomListServiceInterface {
|
||||
override suspend fun allRooms(): RoomList {
|
||||
return RoomList(Pointer.NULL)
|
||||
}
|
||||
|
||||
override suspend fun applyInput(input: RoomListInput) = Unit
|
||||
|
||||
override suspend fun invites(): RoomList {
|
||||
return RoomList(Pointer.NULL)
|
||||
}
|
||||
|
||||
override fun room(roomId: String): RoomListItem {
|
||||
return RoomListItem(Pointer.NULL)
|
||||
}
|
||||
|
||||
override fun state(listener: RoomListServiceStateListener): TaskHandle {
|
||||
return TaskHandle(Pointer.NULL)
|
||||
}
|
||||
|
||||
override fun syncIndicator(delayBeforeShowingInMs: UInt, delayBeforeHidingInMs: UInt, listener: RoomListServiceSyncIndicatorListener): TaskHandle {
|
||||
return TaskHandle(Pointer.NULL)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue