Merge branch 'develop' into feature/fga/pinned_messages_list
This commit is contained in:
commit
12e7e05551
289 changed files with 3558 additions and 1883 deletions
|
|
@ -31,4 +31,6 @@ dependencies {
|
|||
api(libs.maplibre)
|
||||
api(libs.maplibre.ktx)
|
||||
api(libs.maplibre.annotation)
|
||||
// needed for libs.maplibre.annotation waiting for a new release with the fix
|
||||
implementation(libs.mapbox.android.gestures)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,9 +88,10 @@ interface MatrixClient : Closeable {
|
|||
* Logout the user.
|
||||
* Returns an optional URL. When the URL is there, it should be presented to the user after logout for
|
||||
* Relying Party (RP) initiated logout on their account page.
|
||||
* @param userInitiated if false, the logout came from the HS, no request will be made and the session entry will be kept in the store.
|
||||
* @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway.
|
||||
*/
|
||||
suspend fun logout(ignoreSdkError: Boolean): String?
|
||||
suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String?
|
||||
|
||||
/**
|
||||
* Retrieve the user profile, will also eventually emit a new value to [userProfile].
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ package io.element.android.libraries.matrix.api.auth
|
|||
|
||||
sealed class AuthenticationException(message: String) : Exception(message) {
|
||||
class InvalidServerName(message: String) : AuthenticationException(message)
|
||||
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
|
||||
class SlidingSyncVersion(message: String) : AuthenticationException(message)
|
||||
class Generic(message: String) : AuthenticationException(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.api.core
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
@JvmInline
|
||||
value class UniqueId(val value: String) : Serializable {
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ interface EncryptionService {
|
|||
/**
|
||||
* A handle to reset the user's identity.
|
||||
*/
|
||||
interface IdentityResetHandle {
|
||||
sealed interface IdentityResetHandle {
|
||||
/**
|
||||
* Cancel the reset process and drops the existing handle in the SDK.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ sealed interface NotificationContent {
|
|||
) : MessageLike
|
||||
|
||||
data class RoomRedaction(
|
||||
val redactedEventId: String?,
|
||||
val reason: String?
|
||||
val redactedEventId: EventId?,
|
||||
val reason: String?,
|
||||
) : MessageLike
|
||||
|
||||
data object Sticker : MessageLike
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
|
|
@ -155,7 +156,7 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit>
|
||||
suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit>
|
||||
|
||||
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,15 +18,16 @@ package io.element.android.libraries.matrix.api.timeline
|
|||
|
||||
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.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
|
||||
sealed interface MatrixTimelineItem {
|
||||
data class Event(val uniqueId: String, val event: EventTimelineItem) : MatrixTimelineItem {
|
||||
data class Event(val uniqueId: UniqueId, val event: EventTimelineItem) : MatrixTimelineItem {
|
||||
val eventId: EventId? = event.eventId
|
||||
val transactionId: TransactionId? = event.transactionId
|
||||
}
|
||||
|
||||
data class Virtual(val uniqueId: String, val virtual: VirtualTimelineItem) : MatrixTimelineItem
|
||||
data class Virtual(val uniqueId: UniqueId, val virtual: VirtualTimelineItem) : MatrixTimelineItem
|
||||
data object Other : MatrixTimelineItem
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
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.ImageInfo
|
||||
|
|
@ -103,7 +104,7 @@ interface Timeline : AutoCloseable {
|
|||
|
||||
suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
|
||||
|
||||
suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit>
|
||||
suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit>
|
||||
|
||||
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ data class TracingFilterConfiguration(
|
|||
Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.INFO,
|
||||
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.INFO,
|
||||
Target.MATRIX_SDK_UI_TIMELINE to LogLevel.INFO,
|
||||
Target.MATRIX_SDK_BASE_CLIENT to LogLevel.TRACE,
|
||||
// To debug OIDC logouts
|
||||
Target.MATRIX_SDK_OIDC to LogLevel.TRACE,
|
||||
)
|
||||
|
|
@ -69,6 +70,7 @@ enum class Target(open val filter: String) {
|
|||
MATRIX_SDK_BASE_SLIDING_SYNC("matrix_sdk_base::sliding_sync"),
|
||||
MATRIX_SDK_UI_TIMELINE("matrix_sdk_ui::timeline"),
|
||||
MATRIX_SDK_BASE_READ_RECEIPTS("matrix_sdk_base::read_receipts"),
|
||||
MATRIX_SDK_BASE_CLIENT("matrix_sdk_base"),
|
||||
}
|
||||
|
||||
enum class LogLevel(open val filter: String) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import org.matrix.rustcomponents.sdk.ClientSessionDelegate
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* This class is responsible for handling the session data for the Rust SDK.
|
||||
*
|
||||
* It implements both [ClientSessionDelegate] and [ClientDelegate] to react to session data updates and auth errors.
|
||||
*
|
||||
* IMPORTANT: you must set the [client] property as soon as possible so [didReceiveAuthError] can work properly.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class RustClientSessionDelegate(
|
||||
private val sessionStore: SessionStore,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
) : ClientSessionDelegate, ClientDelegate {
|
||||
private val clientLog = Timber.tag("$this")
|
||||
|
||||
// Used to ensure several calls to `didReceiveAuthError` don't trigger multiple logouts
|
||||
private val isLoggingOut = AtomicBoolean(false)
|
||||
|
||||
// To make sure only one coroutine affecting the token persistence can run at a time
|
||||
private val updateTokensDispatcher = coroutineDispatchers.io.limitedParallelism(1)
|
||||
|
||||
// This Client needs to be set up as soon as possible so `didReceiveAuthError` can work properly.
|
||||
private var client: RustMatrixClient? = null
|
||||
|
||||
/**
|
||||
* Sets the [ClientDelegate] for the [RustMatrixClient], and keeps a reference to the client so it can be used later.
|
||||
*/
|
||||
fun bindClient(client: RustMatrixClient) {
|
||||
this.client = client
|
||||
client.setDelegate(this)
|
||||
}
|
||||
|
||||
override fun saveSessionInKeychain(session: Session) {
|
||||
appCoroutineScope.launch(updateTokensDispatcher) {
|
||||
val existingData = sessionStore.getSession(session.userId) ?: return@launch
|
||||
val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens()
|
||||
clientLog.d(
|
||||
"Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " +
|
||||
"Was token valid: ${existingData.isTokenValid}"
|
||||
)
|
||||
val newData = session.toSessionData(
|
||||
isTokenValid = true,
|
||||
loginType = existingData.loginType,
|
||||
passphrase = existingData.passphrase,
|
||||
sessionPaths = existingData.getSessionPaths(),
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
clientLog.d("Saved new session data with access token: '$anonymizedAccessToken'.")
|
||||
}.invokeOnCompletion {
|
||||
if (it != null) {
|
||||
clientLog.e(it, "Failed to save new session data.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun didReceiveAuthError(isSoftLogout: Boolean) {
|
||||
clientLog.w("didReceiveAuthError(isSoftLogout=$isSoftLogout)")
|
||||
if (isLoggingOut.getAndSet(true).not()) {
|
||||
clientLog.v("didReceiveAuthError -> do the cleanup")
|
||||
// TODO handle isSoftLogout parameter.
|
||||
appCoroutineScope.launch(updateTokensDispatcher) {
|
||||
val currentClient = client
|
||||
if (currentClient == null) {
|
||||
clientLog.w("didReceiveAuthError -> no client, exiting")
|
||||
isLoggingOut.set(false)
|
||||
return@launch
|
||||
}
|
||||
val existingData = sessionStore.getSession(currentClient.sessionId.value)
|
||||
val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens()
|
||||
clientLog.d(
|
||||
"Removing session data with access token '$anonymizedAccessToken' " +
|
||||
"and refresh token '$anonymizedRefreshToken'."
|
||||
)
|
||||
if (existingData != null) {
|
||||
// Set isTokenValid to false
|
||||
val newData = existingData.copy(isTokenValid = false)
|
||||
sessionStore.updateData(newData)
|
||||
clientLog.d("Invalidated session data with access token: '$anonymizedAccessToken'.")
|
||||
} else {
|
||||
clientLog.d("No session data found.")
|
||||
}
|
||||
client?.logout(userInitiated = false, ignoreSdkError = true)
|
||||
}.invokeOnCompletion {
|
||||
if (it != null) {
|
||||
clientLog.e(it, "Failed to remove session data.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clientLog.v("didReceiveAuthError -> already cleaning up")
|
||||
}
|
||||
}
|
||||
|
||||
override fun didRefreshTokens() {
|
||||
// This is done in `saveSessionInKeychain(Session)` instead.
|
||||
}
|
||||
|
||||
override fun retrieveSessionFromKeychain(userId: String): Session {
|
||||
// This should never be called, as it's only used for multi-process setups
|
||||
error("retrieveSessionFromKeychain should never be called for Android")
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +51,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
||||
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
|
||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
||||
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
||||
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
|
||||
|
|
@ -67,8 +66,7 @@ import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
|
|||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
|
||||
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
|
||||
import io.element.android.libraries.matrix.impl.util.SessionDirectoryProvider
|
||||
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
|
||||
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
||||
|
|
@ -99,7 +97,6 @@ import kotlinx.coroutines.withContext
|
|||
import kotlinx.coroutines.withTimeout
|
||||
import org.matrix.rustcomponents.sdk.BackupState
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import org.matrix.rustcomponents.sdk.ClientException
|
||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||
|
|
@ -110,7 +107,6 @@ import org.matrix.rustcomponents.sdk.use
|
|||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.INFINITE
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
|
@ -129,6 +125,7 @@ class RustMatrixClient(
|
|||
private val baseDirectory: File,
|
||||
baseCacheDirectory: File,
|
||||
private val clock: SystemClock,
|
||||
sessionDelegate: RustClientSessionDelegate,
|
||||
) : MatrixClient {
|
||||
override val sessionId: UserId = UserId(client.userId())
|
||||
override val deviceId: String = client.deviceId()
|
||||
|
|
@ -137,8 +134,6 @@ class RustMatrixClient(
|
|||
private val innerRoomListService = syncService.roomListService()
|
||||
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
||||
|
||||
// To make sure only one coroutine affecting the token persistence can run at a time
|
||||
private val tokenRefreshDispatcher = sessionDispatcher.limitedParallelism(1)
|
||||
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
|
||||
private val pushersService = RustPushersService(
|
||||
client = client,
|
||||
|
|
@ -161,73 +156,7 @@ class RustMatrixClient(
|
|||
sessionDispatcher = sessionDispatcher,
|
||||
)
|
||||
|
||||
private val sessionDirectoryProvider = SessionDirectoryProvider(sessionStore)
|
||||
|
||||
private val isLoggingOut = AtomicBoolean(false)
|
||||
|
||||
private val clientDelegate = object : ClientDelegate {
|
||||
private val clientLog get() = Timber.tag(this@RustMatrixClient.toString())
|
||||
|
||||
override fun didReceiveAuthError(isSoftLogout: Boolean) {
|
||||
clientLog.w("didReceiveAuthError(isSoftLogout=$isSoftLogout)")
|
||||
if (isLoggingOut.getAndSet(true).not()) {
|
||||
clientLog.v("didReceiveAuthError -> do the cleanup")
|
||||
// TODO handle isSoftLogout parameter.
|
||||
appCoroutineScope.launch(tokenRefreshDispatcher) {
|
||||
val existingData = sessionStore.getSession(client.userId())
|
||||
val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens()
|
||||
clientLog.d(
|
||||
"Removing session data with access token '$anonymizedAccessToken' " +
|
||||
"and refresh token '$anonymizedRefreshToken'."
|
||||
)
|
||||
if (existingData != null) {
|
||||
// Set isTokenValid to false
|
||||
val newData = client.session().toSessionData(
|
||||
isTokenValid = false,
|
||||
loginType = existingData.loginType,
|
||||
passphrase = existingData.passphrase,
|
||||
sessionPath = existingData.sessionPath,
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
clientLog.d("Removed session data with access token: '$anonymizedAccessToken'.")
|
||||
} else {
|
||||
clientLog.d("No session data found.")
|
||||
}
|
||||
doLogout(doRequest = false, removeSession = false, ignoreSdkError = false)
|
||||
}.invokeOnCompletion {
|
||||
if (it != null) {
|
||||
clientLog.e(it, "Failed to remove session data.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clientLog.v("didReceiveAuthError -> already cleaning up")
|
||||
}
|
||||
}
|
||||
|
||||
override fun didRefreshTokens() {
|
||||
clientLog.w("didRefreshTokens()")
|
||||
appCoroutineScope.launch(tokenRefreshDispatcher) {
|
||||
val existingData = sessionStore.getSession(client.userId()) ?: return@launch
|
||||
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
||||
clientLog.d(
|
||||
"Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " +
|
||||
"Was token valid: ${existingData.isTokenValid}"
|
||||
)
|
||||
val newData = client.session().toSessionData(
|
||||
isTokenValid = true,
|
||||
loginType = existingData.loginType,
|
||||
passphrase = existingData.passphrase,
|
||||
sessionPath = existingData.sessionPath,
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
clientLog.d("Saved new session data with access token: '$anonymizedAccessToken'.")
|
||||
}.invokeOnCompletion {
|
||||
if (it != null) {
|
||||
clientLog.e(it, "Failed to save new session data.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val sessionPathsProvider = SessionPathsProvider(sessionStore)
|
||||
|
||||
private val roomSyncSubscriber: RoomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers)
|
||||
|
||||
|
|
@ -270,7 +199,7 @@ class RustMatrixClient(
|
|||
|
||||
private val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(clientDelegate)
|
||||
private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(sessionDelegate)
|
||||
|
||||
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(
|
||||
MatrixUser(
|
||||
|
|
@ -294,6 +223,9 @@ class RustMatrixClient(
|
|||
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
|
||||
|
||||
init {
|
||||
// Make sure the session delegate has a reference to the client to be able to logout on auth error
|
||||
sessionDelegate.bindClient(this)
|
||||
|
||||
sessionCoroutineScope.launch {
|
||||
// Force a refresh of the profile
|
||||
getUserProfile()
|
||||
|
|
@ -535,21 +467,11 @@ class RustMatrixClient(
|
|||
deleteSessionDirectory(deleteCryptoDb = false)
|
||||
}
|
||||
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? = doLogout(
|
||||
doRequest = true,
|
||||
removeSession = true,
|
||||
ignoreSdkError = ignoreSdkError,
|
||||
)
|
||||
|
||||
private suspend fun doLogout(
|
||||
doRequest: Boolean,
|
||||
removeSession: Boolean,
|
||||
ignoreSdkError: Boolean,
|
||||
): String? {
|
||||
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? {
|
||||
var result: String? = null
|
||||
syncService.stop()
|
||||
withContext(sessionDispatcher) {
|
||||
if (doRequest) {
|
||||
if (userInitiated) {
|
||||
try {
|
||||
result = client.logout()
|
||||
} catch (failure: Throwable) {
|
||||
|
|
@ -563,7 +485,7 @@ class RustMatrixClient(
|
|||
}
|
||||
close()
|
||||
deleteSessionDirectory(deleteCryptoDb = true)
|
||||
if (removeSession) {
|
||||
if (userInitiated) {
|
||||
sessionStore.removeSession(sessionId.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -614,19 +536,24 @@ class RustMatrixClient(
|
|||
})
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
internal fun setDelegate(delegate: RustClientSessionDelegate) {
|
||||
client.setDelegate(delegate)
|
||||
}
|
||||
|
||||
private suspend fun File.getCacheSize(
|
||||
includeCryptoDb: Boolean = false,
|
||||
): Long = withContext(sessionDispatcher) {
|
||||
val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext 0L
|
||||
val sessionDirectory = sessionPathsProvider.provides(sessionId) ?: return@withContext 0L
|
||||
val cacheSize = sessionDirectory.cacheDirectory.getSizeOfFiles()
|
||||
if (includeCryptoDb) {
|
||||
sessionDirectory.getSizeOfFiles()
|
||||
cacheSize + sessionDirectory.fileDirectory.getSizeOfFiles()
|
||||
} else {
|
||||
listOf(
|
||||
cacheSize + listOf(
|
||||
"matrix-sdk-state.sqlite3",
|
||||
"matrix-sdk-state.sqlite3-shm",
|
||||
"matrix-sdk-state.sqlite3-wal",
|
||||
).map { fileName ->
|
||||
File(sessionDirectory, fileName)
|
||||
File(sessionDirectory.fileDirectory, fileName)
|
||||
}.sumOf { file ->
|
||||
file.length()
|
||||
}
|
||||
|
|
@ -636,14 +563,16 @@ class RustMatrixClient(
|
|||
private suspend fun deleteSessionDirectory(
|
||||
deleteCryptoDb: Boolean = false,
|
||||
): Boolean = withContext(sessionDispatcher) {
|
||||
val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext false
|
||||
val sessionPaths = sessionPathsProvider.provides(sessionId) ?: return@withContext false
|
||||
// Always delete the cache directory
|
||||
sessionPaths.cacheDirectory.deleteRecursively()
|
||||
if (deleteCryptoDb) {
|
||||
// Delete the folder and all its content
|
||||
sessionDirectory.deleteRecursively()
|
||||
sessionPaths.fileDirectory.deleteRecursively()
|
||||
} else {
|
||||
// Delete only the state.db file
|
||||
sessionDirectory.listFiles().orEmpty()
|
||||
.filter { it.name.contains("matrix-sdk-state") }
|
||||
// Do not delete the crypto database files.
|
||||
sessionPaths.fileDirectory.listFiles().orEmpty()
|
||||
.filterNot { it.name.contains("matrix-sdk-crypto") }
|
||||
.forEach { file ->
|
||||
Timber.w("Deleting file ${file.name}...")
|
||||
file.safeDelete()
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
import io.element.android.appconfig.AuthenticationConfig
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.CacheDirectory
|
||||
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
|
||||
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
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
|
||||
|
|
@ -32,6 +35,8 @@ import kotlinx.coroutines.flow.first
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
|
@ -51,17 +56,20 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) {
|
||||
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
|
||||
val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
|
||||
|
||||
val client = getBaseClientBuilder(
|
||||
sessionPath = sessionData.sessionPath,
|
||||
sessionPaths = sessionData.getSessionPaths(),
|
||||
passphrase = sessionData.passphrase,
|
||||
slidingSync = if (appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()) {
|
||||
ClientBuilderSlidingSync.Simplified
|
||||
} else {
|
||||
ClientBuilderSlidingSync.Restored
|
||||
},
|
||||
slidingSync = when {
|
||||
AuthenticationConfig.SLIDING_SYNC_PROXY_URL != null -> ClientBuilderSlidingSync.CustomProxy(AuthenticationConfig.SLIDING_SYNC_PROXY_URL!!)
|
||||
appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first() -> ClientBuilderSlidingSync.Simplified
|
||||
else -> ClientBuilderSlidingSync.Restored
|
||||
}
|
||||
)
|
||||
.homeserverUrl(sessionData.homeserverUrl)
|
||||
.username(sessionData.userId)
|
||||
.setSessionDelegate(sessionDelegate)
|
||||
.use { it.build() }
|
||||
|
||||
client.restoreSession(sessionData.toSession())
|
||||
|
|
@ -81,31 +89,34 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
baseDirectory = baseDirectory,
|
||||
baseCacheDirectory = cacheDirectory,
|
||||
clock = clock,
|
||||
sessionDelegate = sessionDelegate,
|
||||
).also {
|
||||
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getBaseClientBuilder(
|
||||
sessionPath: String,
|
||||
sessionPaths: SessionPaths,
|
||||
passphrase: String?,
|
||||
slidingSyncProxy: String? = null,
|
||||
slidingSync: ClientBuilderSlidingSync,
|
||||
): ClientBuilder {
|
||||
return ClientBuilder()
|
||||
// TODO SDK claims it's valid to use the same path for data and cache, but would be better to use different paths
|
||||
.sessionPaths(dataPath = sessionPath, cachePath = sessionPath)
|
||||
.sessionPaths(
|
||||
dataPath = sessionPaths.fileDirectory.absolutePath,
|
||||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||
)
|
||||
.passphrase(passphrase)
|
||||
.slidingSyncProxy(slidingSyncProxy)
|
||||
.userAgent(userAgentProvider.provide())
|
||||
.addRootCertificates(userCertificatesProvider.provides())
|
||||
.autoEnableBackups(true)
|
||||
.autoEnableCrossSigning(true)
|
||||
.run {
|
||||
// Apply sliding sync version settings
|
||||
when (slidingSync) {
|
||||
ClientBuilderSlidingSync.Restored -> this
|
||||
ClientBuilderSlidingSync.Discovered -> requiresSlidingSync()
|
||||
ClientBuilderSlidingSync.Simplified -> simplifiedSlidingSync(true)
|
||||
is ClientBuilderSlidingSync.CustomProxy -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Proxy(slidingSync.url))
|
||||
ClientBuilderSlidingSync.Discovered -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.DiscoverProxy)
|
||||
ClientBuilderSlidingSync.Simplified -> slidingSyncVersionBuilder(SlidingSyncVersionBuilder.Native)
|
||||
}
|
||||
}
|
||||
.run {
|
||||
|
|
@ -115,15 +126,18 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
enum class ClientBuilderSlidingSync {
|
||||
sealed interface ClientBuilderSlidingSync {
|
||||
// The proxy is set by the user.
|
||||
data class CustomProxy(val url: String) : ClientBuilderSlidingSync
|
||||
|
||||
// The proxy will be supplied when restoring the Session.
|
||||
Restored,
|
||||
data object Restored : ClientBuilderSlidingSync
|
||||
|
||||
// A proxy must be discovered whilst building the session.
|
||||
Discovered,
|
||||
data object Discovered : ClientBuilderSlidingSync
|
||||
|
||||
// Use Simplified Sliding Sync (discovery isn't a thing yet).
|
||||
Simplified,
|
||||
data object Simplified : ClientBuilderSlidingSync
|
||||
}
|
||||
|
||||
private fun SessionData.toSession() = Session(
|
||||
|
|
@ -132,6 +146,6 @@ private fun SessionData.toSession() = Session(
|
|||
userId = userId,
|
||||
deviceId = deviceId,
|
||||
homeserverUrl = homeserverUrl,
|
||||
slidingSyncProxy = slidingSyncProxy,
|
||||
slidingSyncVersion = slidingSyncProxy?.let(SlidingSyncVersion::Proxy) ?: SlidingSyncVersion.Native,
|
||||
oidcData = oidcData,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
|||
return when (this) {
|
||||
is RustAuthenticationException.Generic -> AuthenticationException.Generic(message)
|
||||
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
|
||||
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(message)
|
||||
is RustAuthenticationException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message)
|
||||
else -> AuthenticationException.Generic(message)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.libraries.matrix.impl.auth
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.appconfig.AuthenticationConfig
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
|
@ -37,6 +36,8 @@ import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
|
|||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
|
||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
|
|
@ -53,14 +54,12 @@ import org.matrix.rustcomponents.sdk.QrLoginProgressListener
|
|||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk.OidcAuthorizationData
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class RustMatrixAuthenticationService @Inject constructor(
|
||||
private val baseDirectory: File,
|
||||
private val sessionPathsFactory: SessionPathsFactory,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val sessionStore: SessionStore,
|
||||
private val rustMatrixClientFactory: RustMatrixClientFactory,
|
||||
|
|
@ -73,14 +72,14 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
|
||||
// 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 sessionPaths: SessionPaths? = 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 }
|
||||
private fun rotateSessionPath(): SessionPaths {
|
||||
sessionPaths?.deleteRecursively()
|
||||
return sessionPathsFactory.create()
|
||||
.also { sessionPaths = it }
|
||||
}
|
||||
|
||||
override fun loggedInStateFlow(): Flow<LoggedInState> {
|
||||
|
|
@ -145,14 +144,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")
|
||||
val currentSessionPaths = sessionPaths ?: 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 = currentSessionPath.absolutePath,
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
clear()
|
||||
sessionStore.storeData(sessionData)
|
||||
|
|
@ -196,14 +195,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 currentSessionPaths = sessionPaths ?: 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 = currentSessionPath.absolutePath,
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
clear()
|
||||
pendingOidcAuthorizationData?.close()
|
||||
|
|
@ -218,12 +217,11 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
|
||||
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
val emptySessionPath = rotateSessionPath()
|
||||
val emptySessionPaths = rotateSessionPath()
|
||||
runCatching {
|
||||
val client = rustMatrixClientFactory.getBaseClientBuilder(
|
||||
sessionPath = emptySessionPath.absolutePath,
|
||||
sessionPaths = emptySessionPaths,
|
||||
passphrase = pendingPassphrase,
|
||||
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
|
||||
slidingSync = ClientBuilderSlidingSync.Discovered,
|
||||
)
|
||||
.buildWithQrCode(
|
||||
|
|
@ -242,7 +240,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
isTokenValid = true,
|
||||
loginType = LoginType.QR,
|
||||
passphrase = pendingPassphrase,
|
||||
sessionPath = emptySessionPath.absolutePath,
|
||||
sessionPaths = emptySessionPaths,
|
||||
)
|
||||
sessionStore.storeData(sessionData)
|
||||
SessionId(sessionData.userId)
|
||||
|
|
@ -262,12 +260,11 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getBaseClientBuilder(
|
||||
sessionPath: File,
|
||||
sessionPaths: SessionPaths,
|
||||
) = rustMatrixClientFactory
|
||||
.getBaseClientBuilder(
|
||||
sessionPath = sessionPath.absolutePath,
|
||||
sessionPaths = sessionPaths,
|
||||
passphrase = pendingPassphrase,
|
||||
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
|
||||
slidingSync = ClientBuilderSlidingSync.Discovered,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.encryption
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
|
|
@ -204,9 +205,9 @@ internal class RustEncryptionService(
|
|||
|
||||
override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
|
||||
return runCatching {
|
||||
service.resetIdentity()?.let { handle ->
|
||||
RustIdentityResetHandleFactory.create(sessionId, handle)
|
||||
}?.getOrNull()
|
||||
service.resetIdentity()
|
||||
}.flatMap { handle ->
|
||||
RustIdentityResetHandleFactory.create(sessionId, handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,13 +27,15 @@ import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType
|
|||
object RustIdentityResetHandleFactory {
|
||||
fun create(
|
||||
userId: UserId,
|
||||
identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle
|
||||
): Result<IdentityResetHandle> {
|
||||
identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle?
|
||||
): Result<IdentityResetHandle?> {
|
||||
return runCatching {
|
||||
when (val authType = identityResetHandle.authType()) {
|
||||
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
|
||||
// User interactive authentication (user + password)
|
||||
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
|
||||
identityResetHandle?.let {
|
||||
when (val authType = identityResetHandle.authType()) {
|
||||
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
|
||||
// User interactive authentication (user + password)
|
||||
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,18 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
|
||||
import java.util.Date
|
||||
|
||||
internal fun Session.toSessionData(
|
||||
isTokenValid: Boolean,
|
||||
loginType: LoginType,
|
||||
passphrase: String?,
|
||||
sessionPath: String,
|
||||
sessionPaths: SessionPaths,
|
||||
homeserverUrl: String? = null,
|
||||
) = SessionData(
|
||||
userId = userId,
|
||||
|
|
@ -34,10 +36,11 @@ internal fun Session.toSessionData(
|
|||
refreshToken = refreshToken,
|
||||
homeserverUrl = homeserverUrl ?: this.homeserverUrl,
|
||||
oidcData = oidcData,
|
||||
slidingSyncProxy = slidingSyncProxy,
|
||||
slidingSyncProxy = (slidingSyncVersion as? SlidingSyncVersion.Proxy)?.url,
|
||||
loginTimestamp = Date(),
|
||||
isTokenValid = isTokenValid,
|
||||
loginType = loginType,
|
||||
passphrase = passphrase,
|
||||
sessionPath = sessionPath,
|
||||
sessionPath = sessionPaths.fileDirectory.absolutePath,
|
||||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.notification
|
||||
|
||||
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.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
|
|
@ -94,7 +95,10 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
|
|||
is MessageLikeEventContent.RoomMessage -> {
|
||||
NotificationContent.MessageLike.RoomMessage(senderId, EventMessageMapper().mapMessageType(messageType))
|
||||
}
|
||||
is MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction(redactedEventId = redactedEventId, reason = reason)
|
||||
is MessageLikeEventContent.RoomRedaction -> NotificationContent.MessageLike.RoomRedaction(
|
||||
redactedEventId = redactedEventId?.let(::EventId),
|
||||
reason = reason,
|
||||
)
|
||||
MessageLikeEventContent.Sticker -> NotificationContent.MessageLike.Sticker
|
||||
is MessageLikeEventContent.Poll -> NotificationContent.MessageLike.Poll(senderId, question)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.paths
|
||||
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import java.io.File
|
||||
|
||||
data class SessionPaths(
|
||||
val fileDirectory: File,
|
||||
val cacheDirectory: File,
|
||||
) {
|
||||
fun deleteRecursively() {
|
||||
fileDirectory.deleteRecursively()
|
||||
cacheDirectory.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SessionData.getSessionPaths(): SessionPaths {
|
||||
return SessionPaths(
|
||||
fileDirectory = File(sessionPath),
|
||||
cacheDirectory = File(cachePath),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.paths
|
||||
|
||||
import io.element.android.libraries.di.CacheDirectory
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
class SessionPathsFactory @Inject constructor(
|
||||
private val baseDirectory: File,
|
||||
@CacheDirectory private val cacheDirectory: File,
|
||||
) {
|
||||
fun create(): SessionPaths {
|
||||
val subPath = UUID.randomUUID().toString()
|
||||
return SessionPaths(
|
||||
fileDirectory = File(baseDirectory, subPath),
|
||||
cacheDirectory = File(cacheDirectory, subPath),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ class RoomSyncSubscriber(
|
|||
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
|
||||
RequiredState(key = EventType.STATE_ROOM_PINNED_EVENT, value = ""),
|
||||
),
|
||||
timelineLimit = DEFAULT_TIMELINE_LIMIT,
|
||||
// We don't need heroes here as they're already included in the `all_rooms` list
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
|
|
@ -459,8 +460,8 @@ class RustMatrixRoom(
|
|||
return liveTimeline.sendFile(file, fileInfo, progressCallback)
|
||||
}
|
||||
|
||||
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> {
|
||||
return liveTimeline.toggleReaction(emoji, eventId)
|
||||
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> {
|
||||
return liveTimeline.toggleReaction(emoji, uniqueId)
|
||||
}
|
||||
|
||||
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.timeline
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||
|
|
@ -31,7 +32,7 @@ class MatrixTimelineItemMapper(
|
|||
private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
|
||||
) {
|
||||
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
|
||||
val uniqueId = timelineItem.uniqueId()
|
||||
val uniqueId = UniqueId(timelineItem.uniqueId())
|
||||
val asEvent = it.asEvent()
|
||||
if (asEvent != null) {
|
||||
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
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.ImageInfo
|
||||
|
|
@ -391,9 +392,9 @@ class RustTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> = withContext(dispatcher) {
|
||||
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
inner.toggleReaction(key = emoji, eventId = eventId.value)
|
||||
inner.toggleReaction(key = emoji, uniqueId = uniqueId.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
|
|
@ -26,7 +27,7 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
|
|||
class LastForwardIndicatorsPostProcessor(
|
||||
private val mode: Timeline.Mode,
|
||||
) {
|
||||
private val lastForwardIdentifiers = LinkedHashSet<String>()
|
||||
private val lastForwardIdentifiers = LinkedHashSet<UniqueId>()
|
||||
|
||||
fun process(
|
||||
items: List<MatrixTimelineItem>,
|
||||
|
|
@ -57,17 +58,17 @@ class LastForwardIndicatorsPostProcessor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createLastForwardIndicator(identifier: String): MatrixTimelineItem {
|
||||
private fun createLastForwardIndicator(identifier: UniqueId): MatrixTimelineItem {
|
||||
return MatrixTimelineItem.Virtual(
|
||||
uniqueId = "last_forward_indicator_$identifier",
|
||||
uniqueId = UniqueId("last_forward_indicator_$identifier"),
|
||||
virtual = VirtualTimelineItem.LastForwardIndicator
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<MatrixTimelineItem>.latestEventIdentifier(): String {
|
||||
private fun List<MatrixTimelineItem>.latestEventIdentifier(): UniqueId {
|
||||
return findLast {
|
||||
it is MatrixTimelineItem.Event
|
||||
}?.let {
|
||||
(it as MatrixTimelineItem.Event).uniqueId
|
||||
} ?: "fake_id"
|
||||
} ?: UniqueId("fake_id")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
|
|
@ -33,7 +34,7 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
|
|||
return buildList {
|
||||
if (shouldAddBackwardLoadingIndicator) {
|
||||
val backwardLoadingIndicator = MatrixTimelineItem.Virtual(
|
||||
uniqueId = "BackwardLoadingIndicator",
|
||||
uniqueId = UniqueId("BackwardLoadingIndicator"),
|
||||
virtual = VirtualTimelineItem.LoadingIndicator(
|
||||
direction = Timeline.PaginationDirection.BACKWARDS,
|
||||
timestamp = currentTimestamp
|
||||
|
|
@ -44,7 +45,7 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
|
|||
addAll(items)
|
||||
if (shouldAddForwardLoadingIndicator) {
|
||||
val forwardLoadingIndicator = MatrixTimelineItem.Virtual(
|
||||
uniqueId = "ForwardLoadingIndicator",
|
||||
uniqueId = UniqueId("ForwardLoadingIndicator"),
|
||||
virtual = VirtualTimelineItem.LoadingIndicator(
|
||||
direction = Timeline.PaginationDirection.FORWARDS,
|
||||
timestamp = currentTimestamp
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
|
|
@ -79,7 +80,7 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) {
|
|||
@VisibleForTesting
|
||||
fun createRoomBeginningItem(): MatrixTimelineItem.Virtual {
|
||||
return MatrixTimelineItem.Virtual(
|
||||
uniqueId = VirtualTimelineItem.RoomBeginning.toString(),
|
||||
uniqueId = UniqueId("RoomBeginning"),
|
||||
virtual = VirtualTimelineItem.RoomBeginning
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
|
@ -23,6 +24,8 @@ import kotlinx.coroutines.withContext
|
|||
import timber.log.Timber
|
||||
import java.util.Date
|
||||
|
||||
internal val encryptedHistoryBannerId = UniqueId("EncryptedHistoryBannerId")
|
||||
|
||||
class TimelineEncryptedHistoryPostProcessor(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val lastLoginTimestamp: Date?,
|
||||
|
|
@ -44,7 +47,7 @@ class TimelineEncryptedHistoryPostProcessor(
|
|||
}
|
||||
return if (lastEncryptedHistoryBannerIndex >= 0) {
|
||||
val sublist = list.drop(lastEncryptedHistoryBannerIndex + 1).toMutableList()
|
||||
sublist.add(0, MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner))
|
||||
sublist.add(0, MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner))
|
||||
sublist
|
||||
} else {
|
||||
list
|
||||
|
|
|
|||
|
|
@ -17,15 +17,15 @@
|
|||
package io.element.android.libraries.matrix.impl.util
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class SessionDirectoryProvider @Inject constructor(
|
||||
class SessionPathsProvider(
|
||||
private val sessionStore: SessionStore,
|
||||
) {
|
||||
suspend fun provides(sessionId: SessionId): File? {
|
||||
val path = sessionStore.getSession(sessionId.value)?.sessionPath ?: return null
|
||||
return File(path)
|
||||
suspend fun provides(sessionId: SessionId): SessionPaths? {
|
||||
val sessionData = sessionStore.getSession(sessionId.value) ?: return null
|
||||
return sessionData.getSessionPaths()
|
||||
}
|
||||
}
|
||||
|
|
@ -48,8 +48,8 @@ class AuthenticationExceptionMappingTest {
|
|||
assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException())
|
||||
.isException<AuthenticationException.Generic>("SDK issue")
|
||||
|
||||
assertThat(ClientBuildException.SlidingSyncNotAvailable("Sliding sync not available").mapAuthenticationException())
|
||||
.isException<AuthenticationException.SlidingSyncNotAvailable>("Sliding sync not available")
|
||||
assertThat(ClientBuildException.SlidingSyncVersion("Sliding sync not available").mapAuthenticationException())
|
||||
.isException<AuthenticationException.SlidingSyncVersion>("Sliding sync not available")
|
||||
}
|
||||
|
||||
private inline fun <reified T> ThrowableSubject.isException(message: String) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
|
|
@ -34,8 +35,8 @@ class RoomBeginningPostProcessorTest {
|
|||
@Test
|
||||
fun `processor removes room creation event and self-join event from DM timeline`() {
|
||||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||
|
|
@ -45,14 +46,20 @@ class RoomBeginningPostProcessorTest {
|
|||
@Test
|
||||
fun `processor removes room creation event and self-join event from DM timeline even if they're not the first items`() {
|
||||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
|
||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event(
|
||||
UniqueId("m.room.member_other"),
|
||||
anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
|
||||
),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.message"), anEventTimelineItem(content = aMessageContent("hi"))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val expected = listOf(
|
||||
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
|
||||
MatrixTimelineItem.Event(
|
||||
UniqueId("m.room.member_other"),
|
||||
anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
|
||||
),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.message"), anEventTimelineItem(content = aMessageContent("hi"))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||
|
|
@ -62,8 +69,8 @@ class RoomBeginningPostProcessorTest {
|
|||
@Test
|
||||
fun `processor will add beginning of room item if it's not a DM`() {
|
||||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
|
||||
|
|
@ -75,7 +82,7 @@ class RoomBeginningPostProcessorTest {
|
|||
@Test
|
||||
fun `processor will not add beginning of room item if it's not a DM and EncryptedHistoryBanner item is found`() {
|
||||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Virtual("EncryptedHistoryBanner", VirtualTimelineItem.EncryptedHistoryBanner),
|
||||
MatrixTimelineItem.Virtual(UniqueId("EncryptedHistoryBanner"), VirtualTimelineItem.EncryptedHistoryBanner),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
|
||||
|
|
@ -85,8 +92,8 @@ class RoomBeginningPostProcessorTest {
|
|||
@Test
|
||||
fun `processor won't remove items if it's not at the start of the timeline`() {
|
||||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||
|
|
@ -96,7 +103,7 @@ class RoomBeginningPostProcessorTest {
|
|||
@Test
|
||||
fun `processor won't remove the first member join event if it can't find the room creation event`() {
|
||||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.member"), anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||
|
|
@ -106,8 +113,11 @@ class RoomBeginningPostProcessorTest {
|
|||
@Test
|
||||
fun `processor won't remove the first member join event if it's not from the room creator`() {
|
||||
val timelineItems = listOf(
|
||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event(UniqueId("m.room.create"), anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event(
|
||||
UniqueId("m.room.member"),
|
||||
anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
|
||||
),
|
||||
)
|
||||
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
|
||||
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
|
@ -26,8 +27,6 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.Test
|
||||
import java.util.Date
|
||||
|
||||
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||
|
||||
class TimelineEncryptedHistoryPostProcessorTest {
|
||||
private val defaultLastLoginTimestamp = Date(1_689_061_264L)
|
||||
|
||||
|
|
@ -35,7 +34,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
|||
fun `given an unencrypted room, nothing is done`() = runTest {
|
||||
val processor = createPostProcessor(isRoomEncrypted = false)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
|
||||
)
|
||||
assertThat(processor.process(items)).isSameInstanceAs(items)
|
||||
}
|
||||
|
|
@ -44,7 +43,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
|||
fun `given an encrypted room, and key backup enabled, nothing is done`() = runTest {
|
||||
val processor = createPostProcessor(isKeyBackupEnabled = true)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
|
||||
)
|
||||
assertThat(processor.process(items)).isSameInstanceAs(items)
|
||||
}
|
||||
|
|
@ -53,7 +52,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
|||
fun `given a null lastLoginTimestamp, nothing is done`() = runTest {
|
||||
val processor = createPostProcessor(lastLoginTimestamp = null)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
|
||||
)
|
||||
assertThat(processor.process(items)).isSameInstanceAs(items)
|
||||
}
|
||||
|
|
@ -69,7 +68,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
|||
fun `given a list with no items before lastLoginTimestamp, nothing is done`() = runTest {
|
||||
val processor = createPostProcessor()
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
|
||||
)
|
||||
assertThat(processor.process(items)).isSameInstanceAs(items)
|
||||
}
|
||||
|
|
@ -78,20 +77,20 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
|||
fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() = runTest {
|
||||
val processor = createPostProcessor()
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
|
||||
)
|
||||
assertThat(processor.process(items))
|
||||
.isEqualTo(listOf(MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner)))
|
||||
.isEqualTo(listOf(MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() = runTest {
|
||||
val processor = createPostProcessor()
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
|
||||
)
|
||||
assertThat(processor.process(items)).isEqualTo(
|
||||
listOf(MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner))
|
||||
listOf(MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -99,14 +98,14 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
|||
fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, then they're replaced`() = runTest {
|
||||
val processor = createPostProcessor()
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)),
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)),
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)),
|
||||
)
|
||||
assertThat(processor.process(items)).isEqualTo(
|
||||
listOf(
|
||||
MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner),
|
||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
|
||||
MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner),
|
||||
MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class FakeMatrixClient(
|
|||
var getRoomInfoFlowLambda = { _: RoomId ->
|
||||
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
|
||||
}
|
||||
var logoutLambda: (Boolean) -> String? = {
|
||||
var logoutLambda: (Boolean, Boolean) -> String? = { _, _ ->
|
||||
null
|
||||
}
|
||||
|
||||
|
|
@ -170,8 +170,8 @@ class FakeMatrixClient(
|
|||
clearCacheLambda()
|
||||
}
|
||||
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask {
|
||||
return logoutLambda(ignoreSdkError)
|
||||
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? = simulateLongTask {
|
||||
return logoutLambda(ignoreSdkError, userInitiated)
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||
|
|
@ -55,7 +56,9 @@ val AN_EVENT_ID = EventId("\$anEventId")
|
|||
val AN_EVENT_ID_2 = EventId("\$anEventId2")
|
||||
val A_ROOM_ALIAS = RoomAlias("#alias1:domain")
|
||||
val A_TRANSACTION_ID = TransactionId("aTransactionId")
|
||||
const val A_UNIQUE_ID = "aUniqueId"
|
||||
|
||||
val A_UNIQUE_ID = UniqueId("aUniqueId")
|
||||
val A_UNIQUE_ID_2 = UniqueId("aUniqueId2")
|
||||
|
||||
const val A_ROOM_NAME = "A room name"
|
||||
const val A_ROOM_RAW_NAME = "A room raw name"
|
||||
|
|
@ -63,6 +66,8 @@ const val A_MESSAGE = "Hello world!"
|
|||
const val A_REPLY = "OK, I'll be there!"
|
||||
const val ANOTHER_MESSAGE = "Hello universe!"
|
||||
|
||||
const val A_REDACTION_REASON = "A redaction reason"
|
||||
|
||||
const val A_HOMESERVER_URL = "matrix.org"
|
||||
const val A_HOMESERVER_URL_2 = "matrix-client.org"
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
|
|
@ -108,7 +109,7 @@ class FakeMatrixRoom(
|
|||
private val editMessageLambda: (EventId, String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _, _ -> lambdaError() },
|
||||
private val sendMessageResult: (String, String?, List<IntentionalMention>) -> Result<Unit> = { _, _, _ -> lambdaError() },
|
||||
private val updateUserRoleResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val toggleReactionResult: (String, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val toggleReactionResult: (String, UniqueId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val retrySendMessageResult: (TransactionId) -> Result<Unit> = { lambdaError() },
|
||||
private val cancelSendResult: (TransactionId) -> Result<Boolean> = { lambdaError() },
|
||||
private val forwardEventResult: (EventId, List<RoomId>) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
|
|
@ -230,8 +231,8 @@ class FakeMatrixRoom(
|
|||
sendMessageResult(body, htmlBody, intentionalMentions)
|
||||
}
|
||||
|
||||
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> {
|
||||
return toggleReactionResult(emoji, eventId)
|
||||
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> {
|
||||
return toggleReactionResult(emoji, uniqueId)
|
||||
}
|
||||
|
||||
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
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.ImageInfo
|
||||
|
|
@ -219,8 +220,8 @@ class FakeTimeline(
|
|||
progressCallback
|
||||
)
|
||||
|
||||
var toggleReactionLambda: (emoji: String, eventId: EventId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> = toggleReactionLambda(emoji, eventId)
|
||||
var toggleReactionLambda: (emoji: String, uniqueId: UniqueId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||
override suspend fun toggleReaction(emoji: String, uniqueId: UniqueId): Result<Unit> = toggleReactionLambda(emoji, uniqueId)
|
||||
|
||||
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.ui.media
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.fetch.Fetcher
|
||||
import coil.request.Options
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
internal class AvatarDataFetcherFactory(
|
||||
private val context: Context,
|
||||
private val client: MatrixClient
|
||||
) : Fetcher.Factory<AvatarData> {
|
||||
override fun create(
|
||||
data: AvatarData,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Fetcher {
|
||||
return CoilMediaFetcher(
|
||||
scalingFunction = { context.resources.displayMetrics.density * it },
|
||||
mediaLoader = client.mediaLoader,
|
||||
mediaData = data.toMediaRequestData(),
|
||||
options = options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
fun AvatarData.toMediaRequestData(): MediaRequestData {
|
||||
internal fun AvatarData.toMediaRequestData(): MediaRequestData {
|
||||
return MediaRequestData(
|
||||
source = url?.let { MediaSource(it) },
|
||||
kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong())
|
||||
|
|
|
|||
|
|
@ -16,16 +16,12 @@
|
|||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.media.toFile
|
||||
|
|
@ -38,11 +34,14 @@ import kotlin.math.roundToLong
|
|||
internal class CoilMediaFetcher(
|
||||
private val scalingFunction: (Float) -> Float,
|
||||
private val mediaLoader: MatrixMediaLoader,
|
||||
private val mediaData: MediaRequestData?,
|
||||
private val mediaData: MediaRequestData,
|
||||
private val options: Options
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
if (mediaData?.source == null) return null
|
||||
if (mediaData.source == null) {
|
||||
Timber.e("MediaData source is null")
|
||||
return null
|
||||
}
|
||||
return when (mediaData.kind) {
|
||||
is MediaRequestData.Kind.Content -> fetchContent(mediaData.source, options)
|
||||
is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind, options)
|
||||
|
|
@ -76,6 +75,8 @@ internal class CoilMediaFetcher(
|
|||
source = mediaSource,
|
||||
).map { byteArray ->
|
||||
byteArray.asSourceResult(options)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +87,8 @@ internal class CoilMediaFetcher(
|
|||
height = scalingFunction(kind.height.toFloat()).roundToLong(),
|
||||
).map { byteArray ->
|
||||
byteArray.asSourceResult(options)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
|
|
@ -102,41 +105,4 @@ internal class CoilMediaFetcher(
|
|||
dataSource = DataSource.MEMORY
|
||||
)
|
||||
}
|
||||
|
||||
class MediaRequestDataFactory(
|
||||
private val context: Context,
|
||||
private val client: MatrixClient
|
||||
) :
|
||||
Fetcher.Factory<MediaRequestData> {
|
||||
override fun create(
|
||||
data: MediaRequestData,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Fetcher {
|
||||
return CoilMediaFetcher(
|
||||
scalingFunction = { context.resources.displayMetrics.density * it },
|
||||
mediaLoader = client.mediaLoader,
|
||||
mediaData = data,
|
||||
options = options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarFactory(
|
||||
private val context: Context,
|
||||
private val client: MatrixClient
|
||||
) : Fetcher.Factory<AvatarData> {
|
||||
override fun create(
|
||||
data: AvatarData,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Fetcher {
|
||||
return CoilMediaFetcher(
|
||||
scalingFunction = { context.resources.displayMetrics.density * it },
|
||||
mediaLoader = client.mediaLoader,
|
||||
mediaData = data.toMediaRequestData(),
|
||||
options = options
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor(
|
|||
}
|
||||
add(AvatarDataKeyer())
|
||||
add(MediaRequestDataKeyer())
|
||||
add(CoilMediaFetcher.AvatarFactory(context, matrixClient))
|
||||
add(CoilMediaFetcher.MediaRequestDataFactory(context, matrixClient))
|
||||
add(AvatarDataFetcherFactory(context, matrixClient))
|
||||
add(MediaRequestDataFetcherFactory(context, matrixClient))
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.ui.media
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.fetch.Fetcher
|
||||
import coil.request.Options
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
internal class MediaRequestDataFetcherFactory(
|
||||
private val context: Context,
|
||||
private val client: MatrixClient
|
||||
) : Fetcher.Factory<MediaRequestData> {
|
||||
override fun create(
|
||||
data: MediaRequestData,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Fetcher {
|
||||
return CoilMediaFetcher(
|
||||
scalingFunction = { context.resources.displayMetrics.density * it },
|
||||
mediaLoader = client.mediaLoader,
|
||||
mediaData = data,
|
||||
options = options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,6 @@ import io.element.android.libraries.push.api.notifications.NotificationIdProvide
|
|||
import javax.inject.Inject
|
||||
|
||||
interface ActiveNotificationsProvider {
|
||||
fun getAllNotifications(): List<StatusBarNotification>
|
||||
fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification>
|
||||
fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification>
|
||||
fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification>
|
||||
|
|
@ -39,10 +38,6 @@ interface ActiveNotificationsProvider {
|
|||
class DefaultActiveNotificationsProvider @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
) : ActiveNotificationsProvider {
|
||||
override fun getAllNotifications(): List<StatusBarNotification> {
|
||||
return notificationManager.activeNotifications
|
||||
}
|
||||
|
||||
override fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
return notificationManager.activeNotifications.filter { it.notification.group == sessionId.value }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ import io.element.android.libraries.matrix.ui.messages.toPlainText
|
|||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
|
|
@ -67,7 +67,7 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No
|
|||
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
|
||||
*/
|
||||
interface NotifiableEventResolver {
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent?
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
|
|
@ -80,7 +80,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
private val permalinkParser: PermalinkParser,
|
||||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
|
||||
// Restore session
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||
val notificationService = client.notificationService()
|
||||
|
|
@ -99,8 +99,9 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
private suspend fun NotificationData.asNotifiableEvent(
|
||||
client: MatrixClient,
|
||||
userId: SessionId,
|
||||
): NotifiableEvent? {
|
||||
return when (val content = this.content) {
|
||||
): ResolvedPushEvent? {
|
||||
val content = this.content
|
||||
val notifiableEvent = when (content) {
|
||||
is NotificationContent.MessageLike.RoomMessage -> {
|
||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
|
||||
|
|
@ -204,8 +205,9 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
NotificationContent.MessageLike.RoomEncrypted -> fallbackNotifiableEvent(userId, roomId, eventId).also {
|
||||
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
|
||||
}
|
||||
is NotificationContent.MessageLike.RoomRedaction -> null.also {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for redaction")
|
||||
is NotificationContent.MessageLike.RoomRedaction -> {
|
||||
// Note: this case will be handled below
|
||||
null
|
||||
}
|
||||
NotificationContent.MessageLike.Sticker -> null.also {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
|
||||
|
|
@ -233,6 +235,25 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
return if (notifiableEvent != null) {
|
||||
ResolvedPushEvent.Event(notifiableEvent)
|
||||
} else if (content is NotificationContent.MessageLike.RoomRedaction) {
|
||||
val redactedEventId = content.redactedEventId
|
||||
if (redactedEventId == null) {
|
||||
Timber.tag(loggerTag.value).d("redactedEventId is null.")
|
||||
null
|
||||
} else {
|
||||
ResolvedPushEvent.Redaction(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
redactedEventId = redactedEventId,
|
||||
reason = content.reason,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun fallbackNotifiableEvent(
|
||||
|
|
|
|||
|
|
@ -42,77 +42,75 @@ class NotificationRenderer @Inject constructor(
|
|||
imageLoader: ImageLoader,
|
||||
) {
|
||||
val groupedEvents = eventsToProcess.groupByType()
|
||||
with(notificationDataFactory) {
|
||||
val roomNotifications = toNotifications(groupedEvents.roomEvents, currentUser, imageLoader)
|
||||
val invitationNotifications = toNotifications(groupedEvents.invitationEvents)
|
||||
val simpleNotifications = toNotifications(groupedEvents.simpleEvents)
|
||||
val fallbackNotifications = toNotifications(groupedEvents.fallbackEvents)
|
||||
val summaryNotification = createSummaryNotification(
|
||||
currentUser = currentUser,
|
||||
roomNotifications = roomNotifications,
|
||||
invitationNotifications = invitationNotifications,
|
||||
simpleNotifications = simpleNotifications,
|
||||
fallbackNotifications = fallbackNotifications,
|
||||
val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, currentUser, imageLoader)
|
||||
val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents)
|
||||
val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents)
|
||||
val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents)
|
||||
val summaryNotification = notificationDataFactory.createSummaryNotification(
|
||||
currentUser = currentUser,
|
||||
roomNotifications = roomNotifications,
|
||||
invitationNotifications = invitationNotifications,
|
||||
simpleNotifications = simpleNotifications,
|
||||
fallbackNotifications = fallbackNotifications,
|
||||
)
|
||||
|
||||
// Remove summary first to avoid briefly displaying it after dismissing the last notification
|
||||
if (summaryNotification == SummaryNotification.Removed) {
|
||||
Timber.tag(loggerTag.value).d("Removing summary notification")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = null,
|
||||
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
|
||||
// Remove summary first to avoid briefly displaying it after dismissing the last notification
|
||||
if (summaryNotification == SummaryNotification.Removed) {
|
||||
Timber.tag(loggerTag.value).d("Removing summary notification")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = null,
|
||||
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
roomNotifications.forEach { notificationData ->
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.roomId.value,
|
||||
id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
|
||||
roomNotifications.forEach { notificationData ->
|
||||
invitationNotifications.forEach { notificationData ->
|
||||
if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.roomId.value,
|
||||
id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
tag = notificationData.key,
|
||||
id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
invitationNotifications.forEach { notificationData ->
|
||||
if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.key,
|
||||
id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
simpleNotifications.forEach { notificationData ->
|
||||
if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.key,
|
||||
id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Show only the first fallback notification
|
||||
if (fallbackNotifications.isNotEmpty()) {
|
||||
Timber.tag(loggerTag.value).d("Showing fallback notification")
|
||||
simpleNotifications.forEach { notificationData ->
|
||||
if (useCompleteNotificationFormat) {
|
||||
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = "FALLBACK",
|
||||
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||
notification = fallbackNotifications.first().notification
|
||||
tag = notificationData.key,
|
||||
id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update summary last to avoid briefly displaying it before other notifications
|
||||
if (summaryNotification is SummaryNotification.Update) {
|
||||
Timber.tag(loggerTag.value).d("Updating summary notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = null,
|
||||
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
||||
notification = summaryNotification.notification
|
||||
)
|
||||
}
|
||||
// Show only the first fallback notification
|
||||
if (fallbackNotifications.isNotEmpty()) {
|
||||
Timber.tag(loggerTag.value).d("Showing fallback notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = "FALLBACK",
|
||||
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||
notification = fallbackNotifications.first().notification
|
||||
)
|
||||
}
|
||||
|
||||
// Update summary last to avoid briefly displaying it before other notifications
|
||||
if (summaryNotification is SummaryNotification.Update) {
|
||||
Timber.tag(loggerTag.value).d("Updating summary notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = null,
|
||||
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
||||
notification = summaryNotification.notification
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -430,6 +430,7 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
event.imageUri?.let {
|
||||
message.setData("image/", it)
|
||||
}
|
||||
message.extras.putString(MESSAGE_EVENT_ID, event.eventId.value)
|
||||
}
|
||||
addMessage(message)
|
||||
}
|
||||
|
|
@ -446,10 +447,10 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
): MessagingStyle {
|
||||
return MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName(user.displayName?.annotateForDebug(50))
|
||||
.setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader))
|
||||
.setKey(sessionId.value)
|
||||
.build()
|
||||
.setName(user.displayName?.annotateForDebug(50))
|
||||
.setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader))
|
||||
.setKey(sessionId.value)
|
||||
.build()
|
||||
).also {
|
||||
it.conversationTitle = roomName.takeIf { roomIsGroup }
|
||||
it.isGroupConversation = roomIsGroup
|
||||
|
|
@ -465,6 +466,10 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MESSAGE_EVENT_ID = "message_event_id"
|
||||
}
|
||||
}
|
||||
|
||||
fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed
|
||||
|
|
|
|||
|
|
@ -19,12 +19,11 @@ package io.element.android.libraries.push.impl.notifications.model
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Parent interface for all events which can be displayed as a Notification.
|
||||
*/
|
||||
sealed interface NotifiableEvent : Serializable {
|
||||
sealed interface NotifiableEvent {
|
||||
val sessionId: SessionId
|
||||
val roomId: RoomId
|
||||
val eventId: EventId
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.push.impl.notifications.model
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface ResolvedPushEvent {
|
||||
data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent
|
||||
|
||||
data class Redaction(
|
||||
val sessionId: SessionId,
|
||||
val roomId: RoomId,
|
||||
val redactedEventId: EventId,
|
||||
val reason: String?,
|
||||
) : ResolvedPushEvent
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
|||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
|
|
@ -41,6 +42,7 @@ private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
|
|||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushHandler @Inject constructor(
|
||||
private val onNotifiableEventReceived: OnNotifiableEventReceived,
|
||||
private val onRedactedEventReceived: OnRedactedEventReceived,
|
||||
private val notifiableEventResolver: NotifiableEventResolver,
|
||||
private val incrementPushDataStore: IncrementPushDataStore,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
|
|
@ -96,19 +98,26 @@ class DefaultPushHandler @Inject constructor(
|
|||
Timber.w("Unable to get a session")
|
||||
return
|
||||
}
|
||||
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
|
||||
when (notifiableEvent) {
|
||||
val resolvedPushEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
|
||||
when (resolvedPushEvent) {
|
||||
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
|
||||
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
|
||||
else -> {
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
is ResolvedPushEvent.Event -> {
|
||||
when (resolvedPushEvent.notifiableEvent) {
|
||||
is NotifiableRingingCallEvent -> handleRingingCallEvent(resolvedPushEvent.notifiableEvent)
|
||||
else -> {
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(resolvedPushEvent.notifiableEvent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ResolvedPushEvent.Redaction -> {
|
||||
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.push.impl.push
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.MessagingStyle
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.factories.DefaultNotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
interface OnRedactedEventReceived {
|
||||
fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultOnRedactedEventReceived @Inject constructor(
|
||||
private val activeNotificationsProvider: ActiveNotificationsProvider,
|
||||
private val notificationDisplayer: NotificationDisplayer,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val stringProvider: StringProvider,
|
||||
) : OnRedactedEventReceived {
|
||||
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) {
|
||||
coroutineScope.launch {
|
||||
val notifications = activeNotificationsProvider.getMessageNotificationsForRoom(
|
||||
redaction.sessionId,
|
||||
redaction.roomId,
|
||||
)
|
||||
if (notifications.isEmpty()) {
|
||||
Timber.d("No notifications found for redacted event")
|
||||
}
|
||||
notifications.forEach { statusBarNotification ->
|
||||
val notification = statusBarNotification.notification
|
||||
val messagingStyle = MessagingStyle.extractMessagingStyleFromNotification(notification)
|
||||
if (messagingStyle == null) {
|
||||
Timber.w("Unable to retrieve messaging style from notification")
|
||||
return@forEach
|
||||
}
|
||||
val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message ->
|
||||
message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) == redaction.redactedEventId.value
|
||||
}
|
||||
if (messageToRedactIndex == -1) {
|
||||
Timber.d("Unable to find the message to remove from notification")
|
||||
return@forEach
|
||||
}
|
||||
val oldMessage = messagingStyle.messages[messageToRedactIndex]
|
||||
val content = buildSpannedString {
|
||||
inSpans(StyleSpan(Typeface.ITALIC)) {
|
||||
append(stringProvider.getString(CommonStrings.common_message_removed))
|
||||
}
|
||||
}
|
||||
val newMessage = MessagingStyle.Message(
|
||||
content,
|
||||
oldMessage.timestamp,
|
||||
oldMessage.person
|
||||
)
|
||||
messagingStyle.messages[messageToRedactIndex] = newMessage
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
statusBarNotification.tag,
|
||||
statusBarNotification.id,
|
||||
NotificationCompat.Builder(context, notification)
|
||||
.setStyle(messagingStyle)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
<string name="notification_room_action_quick_reply">"Réponse rapide"</string>
|
||||
<string name="notification_room_invite_body">"Vous a invité(e) à rejoindre le salon"</string>
|
||||
<string name="notification_sender_me">"Moi"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s mentionné ou en réponse"</string>
|
||||
<string name="notification_test_push_notification_content">"Vous êtes en train de voir la notification ! Cliquez-moi !"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s : %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s : %2$s %3$s"</string>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
<string name="notification_room_action_quick_reply">"Быстрый ответ"</string>
|
||||
<string name="notification_room_invite_body">"Пригласил вас в комнату"</string>
|
||||
<string name="notification_sender_me">"Я"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s упомянул или ответил"</string>
|
||||
<string name="notification_test_push_notification_content">"Вы просматриваете уведомление! Нажмите на меня!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<string name="notification_room_action_quick_reply">"Snabbsvar"</string>
|
||||
<string name="notification_room_invite_body">"Bjöd in dig att gå med i rummet"</string>
|
||||
<string name="notification_sender_me">"Jag"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s nämnde eller svarade"</string>
|
||||
<string name="notification_test_push_notification_content">"Du tittar på aviseringen! Klicka på mig!"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
<string name="notification_ticker_text_group">"%1$s: %2$s %3$s"</string>
|
||||
|
|
|
|||
|
|
@ -36,27 +36,6 @@ import org.robolectric.RobolectricTestRunner
|
|||
class DefaultActiveNotificationsProviderTest {
|
||||
private val notificationIdProvider = NotificationIdProvider
|
||||
|
||||
@Test
|
||||
fun `getAllNotifications with no active notifications returns empty list`() {
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = emptyList())
|
||||
|
||||
val emptyNotifications = activeNotificationsProvider.getAllNotifications()
|
||||
assertThat(emptyNotifications).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAllNotifications with active notifications returns all`() {
|
||||
val activeNotifications = listOf(
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
|
||||
)
|
||||
val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications)
|
||||
|
||||
val result = activeNotificationsProvider.getAllNotifications()
|
||||
assertThat(result).hasSize(3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getNotificationsForSession returns only notifications for that session id`() {
|
||||
val activeNotifications = listOf(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,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_2
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_REDACTION_REASON
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
|
|
@ -52,6 +53,7 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi
|
|||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||
import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
|
||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||
|
|
@ -104,7 +106,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Hello world")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +127,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -146,7 +152,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Hello world")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +177,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Hello world")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +196,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Audio")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Audio")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +215,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Video")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Video")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +234,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Voice message")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Voice message")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +253,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Image")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Image")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +272,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Sticker")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Sticker")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -271,7 +291,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "File")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "File")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -288,7 +310,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Location")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Location")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -305,7 +329,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Notice")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Notice")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +348,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "* Bob is happy")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "* Bob is happy")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +367,9 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = createNotifiableMessageEvent(body = "Poll: A question")
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
createNotifiableMessageEvent(body = "Poll: A question")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
|
|
@ -357,21 +387,23 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
roomName = null,
|
||||
noisy = false,
|
||||
title = null,
|
||||
description = "Invited you to join the room",
|
||||
type = null,
|
||||
timestamp = A_TIMESTAMP,
|
||||
soundName = null,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
roomName = null,
|
||||
noisy = false,
|
||||
title = null,
|
||||
description = "Invited you to join the room",
|
||||
type = null,
|
||||
timestamp = A_TIMESTAMP,
|
||||
soundName = null,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
|
@ -390,21 +422,23 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
roomName = null,
|
||||
noisy = false,
|
||||
title = null,
|
||||
description = "Invited you to chat",
|
||||
type = null,
|
||||
timestamp = A_TIMESTAMP,
|
||||
soundName = null,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
roomName = null,
|
||||
noisy = false,
|
||||
title = null,
|
||||
description = "Invited you to chat",
|
||||
type = null,
|
||||
timestamp = A_TIMESTAMP,
|
||||
soundName = null,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
|
@ -435,16 +469,18 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = FallbackNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
description = "Notification",
|
||||
canBeReplaced = true,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
timestamp = A_FAKE_TIMESTAMP,
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
FallbackNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
description = "Notification",
|
||||
canBeReplaced = true,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
timestamp = A_FAKE_TIMESTAMP,
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
|
@ -459,27 +495,29 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
val expectedResult = NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
canBeReplaced = false,
|
||||
senderId = A_USER_ID_2,
|
||||
noisy = false,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
body = "Call in progress (unsupported)",
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
roomAvatarPath = null,
|
||||
senderAvatarPath = null,
|
||||
soundName = null,
|
||||
outGoingMessage = false,
|
||||
outGoingMessageFailed = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
canBeReplaced = false,
|
||||
senderId = A_USER_ID_2,
|
||||
noisy = false,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
body = "Call in progress (unsupported)",
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
roomAvatarPath = null,
|
||||
senderAvatarPath = null,
|
||||
soundName = null,
|
||||
outGoingMessage = false,
|
||||
outGoingMessageFailed = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
|
@ -498,21 +536,23 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
)
|
||||
val expectedResult = NotifiableRingingCallEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID_2,
|
||||
roomName = null,
|
||||
editedEventId = null,
|
||||
description = "Incoming call",
|
||||
timestamp = timestamp,
|
||||
canBeReplaced = true,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderAvatarUrl = null,
|
||||
callNotifyType = CallNotifyType.RING,
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
senderId = A_USER_ID_2,
|
||||
roomName = null,
|
||||
editedEventId = null,
|
||||
description = "Incoming call",
|
||||
timestamp = timestamp,
|
||||
canBeReplaced = true,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderAvatarUrl = null,
|
||||
callNotifyType = CallNotifyType.RING,
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
|
|
@ -531,22 +571,24 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
)
|
||||
val expectedResult = NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = true,
|
||||
timestamp = 0L,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderId = UserId("@bob:server.org"),
|
||||
body = "☎\uFE0F Incoming call",
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
imageUriString = null,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = true,
|
||||
timestamp = 0L,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderId = UserId("@bob:server.org"),
|
||||
body = "☎\uFE0F Incoming call",
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
imageUriString = null,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
|
|
@ -564,27 +606,67 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
)
|
||||
val expectedResult = NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = true,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderId = UserId("@bob:server.org"),
|
||||
body = "☎\uFE0F Incoming call",
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
imageUriString = null,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
val expectedResult = ResolvedPushEvent.Event(
|
||||
NotifiableMessageEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = true,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
senderId = UserId("@bob:server.org"),
|
||||
body = "☎\uFE0F Incoming call",
|
||||
roomId = A_ROOM_ID,
|
||||
threadId = null,
|
||||
roomName = null,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
imageUriString = null,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve RoomRedaction`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
createNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomRedaction(
|
||||
AN_EVENT_ID_2,
|
||||
A_REDACTION_REASON,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val expectedResult = ResolvedPushEvent.Redaction(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
redactedEventId = AN_EVENT_ID_2,
|
||||
reason = A_REDACTION_REASON,
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve RoomRedaction with null redactedEventId should return null`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
createNotificationData(
|
||||
content = NotificationContent.MessageLike.RoomRedaction(
|
||||
null,
|
||||
A_REDACTION_REASON,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve null cases`() {
|
||||
testNull(NotificationContent.MessageLike.CallAnswer)
|
||||
|
|
@ -598,7 +680,6 @@ class DefaultNotifiableEventResolverTest {
|
|||
testNull(NotificationContent.MessageLike.KeyVerificationMac)
|
||||
testNull(NotificationContent.MessageLike.KeyVerificationDone)
|
||||
testNull(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
|
||||
testNull(NotificationContent.MessageLike.RoomRedaction(redactedEventId = AN_EVENT_ID_2.value, reason = null))
|
||||
testNull(NotificationContent.MessageLike.Sticker)
|
||||
testNull(NotificationContent.StateEvent.PolicyRuleRoom)
|
||||
testNull(NotificationContent.StateEvent.PolicyRuleServer)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class DefaultNotificationDrawerManagerTest {
|
|||
// For now just call all the API. Later, add more valuable tests.
|
||||
val matrixUser = aMatrixUser(id = A_SESSION_ID.value, displayName = "alice", avatarUrl = "mxc://data")
|
||||
val mockRoomGroupMessageCreator = FakeRoomGroupMessageCreator(
|
||||
createRoomMessageResult = lambdaRecorder { user, _, roomId, _, existingNotification, ->
|
||||
createRoomMessageResult = lambdaRecorder { user, _, roomId, _, existingNotification ->
|
||||
assertThat(user).isEqualTo(matrixUser)
|
||||
assertThat(roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(existingNotification).isNull()
|
||||
|
|
@ -167,11 +167,12 @@ class DefaultNotificationDrawerManagerTest {
|
|||
}
|
||||
val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID)
|
||||
val activeNotificationsProvider = FakeActiveNotificationsProvider(
|
||||
mutableListOf(
|
||||
getSummaryNotificationResult = {
|
||||
mockk {
|
||||
every { id } returns summaryId
|
||||
}
|
||||
)
|
||||
},
|
||||
countResult = { 1 },
|
||||
)
|
||||
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
|
||||
notificationManager = notificationManager,
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeNotifiableEventResolver(
|
||||
private val notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> lambdaError() }
|
||||
private val notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() }
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
|
||||
return notifiableEventResult(sessionId, roomId, eventId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,33 +22,34 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||
import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider
|
||||
|
||||
class FakeActiveNotificationsProvider(
|
||||
var activeNotifications: MutableList<StatusBarNotification> = mutableListOf(),
|
||||
private val getMessageNotificationsForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> emptyList() },
|
||||
private val getNotificationsForSessionResult: (SessionId) -> List<StatusBarNotification> = { emptyList() },
|
||||
private val getMembershipNotificationForSessionResult: (SessionId) -> List<StatusBarNotification> = { emptyList() },
|
||||
private val getMembershipNotificationForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> emptyList() },
|
||||
private val getSummaryNotificationResult: (SessionId) -> StatusBarNotification? = { null },
|
||||
private val countResult: (SessionId) -> Int = { 0 },
|
||||
) : ActiveNotificationsProvider {
|
||||
override fun getAllNotifications(): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
}
|
||||
|
||||
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
return getMessageNotificationsForRoomResult(sessionId, roomId)
|
||||
}
|
||||
|
||||
override fun getNotificationsForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
return getNotificationsForSessionResult(sessionId)
|
||||
}
|
||||
|
||||
override fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
return getMembershipNotificationForSessionResult(sessionId)
|
||||
}
|
||||
|
||||
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
return activeNotifications
|
||||
return getMembershipNotificationForRoomResult(sessionId, roomId)
|
||||
}
|
||||
|
||||
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
|
||||
return activeNotifications.firstOrNull()
|
||||
return getSummaryNotificationResult(sessionId)
|
||||
}
|
||||
|
||||
override fun count(sessionId: SessionId): Int {
|
||||
return activeNotifications.size
|
||||
return countResult(sessionId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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
|
||||
*
|
||||
* https://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.push.impl.push
|
||||
|
||||
import android.service.notification.StatusBarNotification
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultOnRedactedEventReceivedTest {
|
||||
@Test
|
||||
fun `when no notifications are found, nothing happen`() = runTest {
|
||||
val sut = createDefaultOnRedactedEventReceived(
|
||||
getMessageNotificationsForRoomResult = { _, _ -> emptyList() }
|
||||
)
|
||||
sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a notification is found, try to retrieve the message`() = runTest {
|
||||
val sut = createDefaultOnRedactedEventReceived(
|
||||
getMessageNotificationsForRoomResult = { _, _ ->
|
||||
listOf(
|
||||
mockk {
|
||||
every { notification } returns mockk {}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultOnRedactedEventReceived(
|
||||
getMessageNotificationsForRoomResult: (SessionId, RoomId) -> List<StatusBarNotification> = { _, _ -> lambdaError() },
|
||||
): DefaultOnRedactedEventReceived {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
return DefaultOnRedactedEventReceived(
|
||||
activeNotificationsProvider = FakeActiveNotificationsProvider(
|
||||
getMessageNotificationsForRoomResult = getMessageNotificationsForRoomResult,
|
||||
getNotificationsForSessionResult = { lambdaError() },
|
||||
getMembershipNotificationForSessionResult = { lambdaError() },
|
||||
getMembershipNotificationForRoomResult = { _, _ -> lambdaError() },
|
||||
getSummaryNotificationResult = { lambdaError() },
|
||||
countResult = { lambdaError() },
|
||||
),
|
||||
notificationDisplayer = FakeNotificationDisplayer(),
|
||||
coroutineScope = this,
|
||||
context = context,
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,8 +30,10 @@ import io.element.android.libraries.matrix.api.core.UserId
|
|||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SECRET
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
|
|
@ -40,6 +42,7 @@ import io.element.android.libraries.push.impl.notifications.channels.FakeNotific
|
|||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
|
|
@ -61,7 +64,9 @@ class DefaultPushHandlerTest {
|
|||
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent> { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
|
|
@ -94,7 +99,9 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
|
|
@ -128,7 +135,9 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
|
|
@ -164,7 +173,9 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
|
|
@ -197,7 +208,7 @@ class DefaultPushHandlerTest {
|
|||
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
|
||||
runTest {
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent?> { _, _, _ -> null }
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event?> { _, _, _ -> null }
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
|
|
@ -240,7 +251,9 @@ class DefaultPushHandlerTest {
|
|||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notifiableEventResult = { _, _, _ -> aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()) },
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()))
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
|
|
@ -265,7 +278,9 @@ class DefaultPushHandlerTest {
|
|||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = { _, _, _ -> aNotifiableMessageEvent(type = EventType.CALL_NOTIFY) },
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY))
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
|
|
@ -291,7 +306,9 @@ class DefaultPushHandlerTest {
|
|||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = { _, _, _ -> aNotifiableCallEvent() },
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableCallEvent())
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
userPushStore = FakeUserPushStore().apply {
|
||||
setNotificationEnabledForDevice(false)
|
||||
|
|
@ -305,6 +322,37 @@ class DefaultPushHandlerTest {
|
|||
onNotifiableEventReceived.assertions().isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a redaction is received, the onRedactedEventReceived is informed`() = runTest {
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val aRedaction = ResolvedPushEvent.Redaction(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
redactedEventId = AN_EVENT_ID_2,
|
||||
reason = null
|
||||
)
|
||||
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { }
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onRedactedEventReceived = onRedactedEventReceived,
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
notifiableEventResult = { _, _, _ -> aRedaction },
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
onRedactedEventReceived.assertions().isCalledOnce()
|
||||
.with(value(aRedaction))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when diagnostic PushData is received, the diagnostic push handler is informed`() =
|
||||
runTest {
|
||||
|
|
@ -327,7 +375,8 @@ class DefaultPushHandlerTest {
|
|||
|
||||
private fun createDefaultPushHandler(
|
||||
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() },
|
||||
notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> lambdaError() },
|
||||
onRedactedEventReceived: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
|
||||
notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() },
|
||||
incrementPushCounterResult: () -> Unit = { lambdaError() },
|
||||
userPushStore: UserPushStore = FakeUserPushStore(),
|
||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||
|
|
@ -339,6 +388,7 @@ class DefaultPushHandlerTest {
|
|||
): DefaultPushHandler {
|
||||
return DefaultPushHandler(
|
||||
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
|
||||
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived),
|
||||
notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult),
|
||||
incrementPushDataStore = object : IncrementPushDataStore {
|
||||
override suspend fun incrementPushCounter() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.push.impl.push
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeOnRedactedEventReceived(
|
||||
private val onRedactedEventReceivedResult: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
|
||||
) : OnRedactedEventReceived {
|
||||
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) {
|
||||
onRedactedEventReceivedResult(redaction)
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,8 @@ data class SessionData(
|
|||
val loginType: LoginType,
|
||||
/** The optional passphrase used to encrypt data in the SDK local store. */
|
||||
val passphrase: String?,
|
||||
/** The path to the session data stored in the filesystem. */
|
||||
/** The paths to the session data stored in the filesystem. */
|
||||
val sessionPath: String,
|
||||
/** The path to the cache data stored for the session in the filesystem. */
|
||||
val cachePath: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
|||
loginType = loginType.name,
|
||||
passphrase = passphrase,
|
||||
sessionPath = sessionPath,
|
||||
cachePath = cachePath,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -52,5 +53,6 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
|||
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
|
||||
passphrase = passphrase,
|
||||
sessionPath = sessionPath,
|
||||
cachePath = cachePath,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -25,7 +25,9 @@ CREATE TABLE SessionData (
|
|||
-- added in version 5
|
||||
passphrase TEXT,
|
||||
-- added in version 6
|
||||
sessionPath TEXT NOT NULL DEFAULT ""
|
||||
sessionPath TEXT NOT NULL DEFAULT "",
|
||||
-- added in version 9
|
||||
cachePath TEXT NOT NULL DEFAULT ""
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
-- Migrate DB from version 8
|
||||
-- Add cachePath so we can track the anonymized path for the session cache dir
|
||||
|
||||
ALTER TABLE SessionData ADD COLUMN cachePath TEXT NOT NULL DEFAULT "";
|
||||
|
|
@ -145,6 +145,7 @@ class DatabaseSessionStoreTest {
|
|||
loginType = null,
|
||||
passphrase = "aPassphrase",
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
)
|
||||
val secondSessionData = SessionData(
|
||||
userId = "userId",
|
||||
|
|
@ -159,6 +160,7 @@ class DatabaseSessionStoreTest {
|
|||
loginType = null,
|
||||
passphrase = "aPassphraseAltered",
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
)
|
||||
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
||||
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
||||
|
|
@ -196,6 +198,7 @@ class DatabaseSessionStoreTest {
|
|||
loginType = null,
|
||||
passphrase = "aPassphrase",
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
)
|
||||
val secondSessionData = SessionData(
|
||||
userId = "userIdUnknown",
|
||||
|
|
@ -210,6 +213,7 @@ class DatabaseSessionStoreTest {
|
|||
loginType = null,
|
||||
passphrase = "aPassphraseAltered",
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
)
|
||||
assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,4 +32,5 @@ internal fun aSessionData() = SessionData(
|
|||
loginType = LoginType.UNKNOWN.name,
|
||||
passphrase = null,
|
||||
sessionPath = "sessionPath",
|
||||
cachePath = "cachePath",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,5 +37,6 @@ fun aSessionData(
|
|||
loginType = LoginType.UNKNOWN,
|
||||
passphrase = null,
|
||||
sessionPath = "/a/path/to/a/session",
|
||||
cachePath = "/a/path/to/a/cache",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,12 +61,12 @@ class MentionSpan(
|
|||
backgroundColor = when (type) {
|
||||
Type.USER -> if (isCurrentUser) mentionSpanTheme.currentUserBackgroundColor else mentionSpanTheme.otherBackgroundColor
|
||||
Type.ROOM -> mentionSpanTheme.otherBackgroundColor
|
||||
Type.EVERYONE -> mentionSpanTheme.otherBackgroundColor
|
||||
Type.EVERYONE -> mentionSpanTheme.currentUserBackgroundColor
|
||||
}
|
||||
textColor = when (type) {
|
||||
Type.USER -> if (isCurrentUser) mentionSpanTheme.currentUserTextColor else mentionSpanTheme.otherTextColor
|
||||
Type.ROOM -> mentionSpanTheme.otherTextColor
|
||||
Type.EVERYONE -> mentionSpanTheme.otherTextColor
|
||||
Type.EVERYONE -> mentionSpanTheme.currentUserTextColor
|
||||
}
|
||||
backgroundPaint.color = backgroundColor
|
||||
val (startPaddingPx, endPaddingPx) = mentionSpanTheme.paddingValuesPx.value
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.libraries.textcomposer.mentions
|
|||
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.text.Spanned
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
|
|
@ -129,6 +130,7 @@ val LocalMentionSpanTheme = staticCompositionLocalOf {
|
|||
eventId = null,
|
||||
viaParameters = persistentListOf(),
|
||||
)
|
||||
"@room" -> PermalinkData.FallbackLink(Uri.EMPTY, false)
|
||||
else -> throw AssertionError("Unexpected value $uriString")
|
||||
}
|
||||
}
|
||||
|
|
@ -139,7 +141,8 @@ val LocalMentionSpanTheme = staticCompositionLocalOf {
|
|||
val textColor = ElementTheme.colors.textPrimary.toArgb()
|
||||
fun mentionSpanMe() = provider.getMentionSpanFor("mention", "https://matrix.to/#/@me:matrix.org")
|
||||
fun mentionSpanOther() = provider.getMentionSpanFor("mention", "https://matrix.to/#/@other:matrix.org")
|
||||
fun mentionSpanRoom() = provider.getMentionSpanFor("room", "https://matrix.to/#/#room:matrix.org")
|
||||
fun mentionSpanRoom() = provider.getMentionSpanFor("room:matrix.org", "https://matrix.to/#/#room:matrix.org")
|
||||
fun mentionSpanEveryone() = provider.getMentionSpanFor("@room", "@room")
|
||||
mentionSpanTheme.updateStyles(currentUserId = UserId("@me:matrix.org"))
|
||||
|
||||
CompositionLocalProvider(
|
||||
|
|
@ -154,7 +157,9 @@ val LocalMentionSpanTheme = staticCompositionLocalOf {
|
|||
append("@mention", mentionSpanMe(), 0)
|
||||
append(" to the current user and this is a ")
|
||||
append("@mention", mentionSpanOther(), 0)
|
||||
append(" to other user. This one is for a room: ")
|
||||
append(" to other user. This is for everyone in the ")
|
||||
append("@room", mentionSpanEveryone(), 0)
|
||||
append(". This one is for a link to another room: ")
|
||||
append("#room:matrix.org", mentionSpanRoom(), 0)
|
||||
append("\n\n")
|
||||
append("This ")
|
||||
|
|
|
|||
|
|
@ -261,6 +261,7 @@ Raison: %1$s."</string>
|
|||
<string name="error_some_messages_have_not_been_sent">"Certains messages n’ont pas été envoyés"</string>
|
||||
<string name="error_unknown">"Désolé, une erreur s’est produite"</string>
|
||||
<string name="event_shield_reason_authenticity_not_guaranteed">"L’authenticité de ce message chiffré ne peut être garantie sur cet appareil."</string>
|
||||
<string name="event_shield_reason_previously_verified">"Chiffré par un utilisateur précédemment vérifié."</string>
|
||||
<string name="event_shield_reason_sent_in_clear">"Non chiffré."</string>
|
||||
<string name="event_shield_reason_unknown_device">"Chiffré par un appareil inconnu ou supprimé."</string>
|
||||
<string name="event_shield_reason_unsigned_device">"Chiffré par un appareil non vérifié par son propriétaire."</string>
|
||||
|
|
|
|||
|
|
@ -268,6 +268,7 @@
|
|||
<string name="error_some_messages_have_not_been_sent">"Некоторые сообщения не были отправлены"</string>
|
||||
<string name="error_unknown">"Извините, произошла ошибка"</string>
|
||||
<string name="event_shield_reason_authenticity_not_guaranteed">"Подлинность этого зашифрованного сообщения не может быть гарантирована на этом устройстве."</string>
|
||||
<string name="event_shield_reason_previously_verified">"Зашифровано ранее проверенным пользователем."</string>
|
||||
<string name="event_shield_reason_sent_in_clear">"Не зашифровано."</string>
|
||||
<string name="event_shield_reason_unknown_device">"Зашифровано неизвестным или удаленным устройством."</string>
|
||||
<string name="event_shield_reason_unsigned_device">"Зашифровано устройством, не проверенным его владельцем."</string>
|
||||
|
|
@ -287,6 +288,8 @@
|
|||
<item quantity="many">"%1$d Закрепленных сообщений"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Закрепленные сообщения"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Вы собираетесь перейти в свою учетную запись %1$s, чтобы сбросить идентификацию. После этого вы вернетесь в приложение."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Не можете подтвердить? Перейдите в свою учетную запись, чтобы сбросить свою идентификацию."</string>
|
||||
<string name="screen_room_details_pinned_events_row_title">"Закрепленные сообщения"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не удалось получить данные о пользователе"</string>
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ Anledning:%1$s."</string>
|
|||
<string name="error_some_messages_have_not_been_sent">"Vissa meddelanden har inte skickats"</string>
|
||||
<string name="error_unknown">"Tyvärr, ett fel uppstod"</string>
|
||||
<string name="event_shield_reason_authenticity_not_guaranteed">"Detta krypterade meddelandes äkthet kan inte garanteras på den här enheten."</string>
|
||||
<string name="event_shield_reason_previously_verified">"Krypterat av en tidigare verifierad användare."</string>
|
||||
<string name="event_shield_reason_sent_in_clear">"Inte krypterad."</string>
|
||||
<string name="event_shield_reason_unknown_device">"Krypterad av en okänd eller raderad enhet."</string>
|
||||
<string name="event_shield_reason_unsigned_device">"Krypterad av en enhet som inte verifierats av ägaren."</string>
|
||||
|
|
@ -280,6 +281,8 @@ Anledning:%1$s."</string>
|
|||
<item quantity="other">"%1$d Fästa meddelanden"</item>
|
||||
</plurals>
|
||||
<string name="screen_pinned_timeline_screen_title_empty">"Fästa meddelanden"</string>
|
||||
<string name="screen_reset_identity_confirmation_subtitle">"Du är på väg att gå till ditt %1$s-konto för att återställa din identitet. Därefter kommer du att tas tillbaka till appen."</string>
|
||||
<string name="screen_reset_identity_confirmation_title">"Kan du inte bekräfta? Gå till ditt konto för att återställa din identitet."</string>
|
||||
<string name="screen_room_details_pinned_events_row_title">"Fästa meddelanden"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Kunde inte hämta användarinformation"</string>
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@
|
|||
<string name="action_invites_list">"Takliflar"</string>
|
||||
<string name="action_join">"Qo\'shilish"</string>
|
||||
<string name="action_learn_more">"Batafsil malumot"</string>
|
||||
<string name="action_leave">"Tark etish "</string>
|
||||
<string name="action_leave_room">"Xonani tark etish "</string>
|
||||
<string name="action_leave">"Tark etish"</string>
|
||||
<string name="action_leave_room">"Xonani tark etish"</string>
|
||||
<string name="action_manage_account">"Hisobni boshqarish"</string>
|
||||
<string name="action_manage_devices">"Qurilmalarni boshqarish"</string>
|
||||
<string name="action_next">"Keyingisi"</string>
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
<string name="action_start_verification">"Tasdiqlashni boshlang"</string>
|
||||
<string name="action_static_map_load">"Xaritani yuklash uchun bosing"</string>
|
||||
<string name="action_take_photo">"Rasmga olmoq"</string>
|
||||
<string name="action_view_source">"Manbani korish "</string>
|
||||
<string name="action_view_source">"Manbani korish"</string>
|
||||
<string name="action_yes">"Ha"</string>
|
||||
<string name="common_about">"Haqida"</string>
|
||||
<string name="common_acceptable_use_policy">"Qabul qilinadigan foydalanish siyosati"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue