Merge remote-tracking branch 'origin/develop' into feature/fre/improve_poll_event_timeline_rendering
This commit is contained in:
commit
e6490b3a89
71 changed files with 674 additions and 189 deletions
|
|
@ -48,13 +48,15 @@ import io.element.android.libraries.theme.LinkColor
|
|||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
|
||||
const val LINK_TAG = "URL"
|
||||
|
||||
@Composable
|
||||
fun ClickableLinkText(
|
||||
text: String,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
linkify: Boolean = true,
|
||||
linkAnnotationTag: String = "",
|
||||
linkAnnotationTag: String = LINK_TAG,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
|
|
@ -80,13 +82,14 @@ fun ClickableLinkText(
|
|||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
linkify: Boolean = true,
|
||||
linkAnnotationTag: String = "",
|
||||
linkAnnotationTag: String = LINK_TAG,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
inlineContent: ImmutableMap<String, InlineTextContent> = persistentMapOf(),
|
||||
) {
|
||||
val processedText = remember(annotatedString) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val annotatedString = remember(annotatedString) {
|
||||
if (linkify) {
|
||||
annotatedString.linkify(SpanStyle(color = LinkColor))
|
||||
} else {
|
||||
|
|
@ -126,7 +129,7 @@ fun ClickableLinkText(
|
|||
}
|
||||
}
|
||||
Text(
|
||||
text = processedText,
|
||||
text = annotatedString,
|
||||
modifier = modifier.then(pressIndicator),
|
||||
style = style,
|
||||
onTextLayout = {
|
||||
|
|
@ -158,7 +161,7 @@ fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
|||
style = linkStyle,
|
||||
)
|
||||
addStringAnnotation(
|
||||
tag = "URL",
|
||||
tag = LINK_TAG,
|
||||
annotation = span.url,
|
||||
start = start,
|
||||
end = end
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -133,32 +134,46 @@ internal fun ButtonInternal(
|
|||
ButtonSize.Large -> 48.dp
|
||||
}
|
||||
|
||||
val hasStartDrawable = showProgress || leadingIcon != null
|
||||
|
||||
val contentPadding = when (size) {
|
||||
ButtonSize.Medium -> {
|
||||
when (style) {
|
||||
ButtonStyle.Text -> PaddingValues(horizontal = 12.dp, vertical = 10.dp)
|
||||
else -> PaddingValues(horizontal = 16.dp, vertical = 10.dp)
|
||||
}
|
||||
ButtonSize.Medium -> when (style) {
|
||||
ButtonStyle.Filled,
|
||||
ButtonStyle.Outlined -> if (hasStartDrawable)
|
||||
PaddingValues(start = 16.dp, top = 10.dp, end = 24.dp, bottom = 10.dp)
|
||||
else
|
||||
PaddingValues(start = 24.dp, top = 10.dp, end = 24.dp, bottom = 10.dp)
|
||||
ButtonStyle.Text -> if (hasStartDrawable)
|
||||
PaddingValues(start = 12.dp, top = 10.dp, end = 16.dp, bottom = 10.dp)
|
||||
else
|
||||
PaddingValues(start = 12.dp, top = 10.dp, end = 12.dp, bottom = 10.dp)
|
||||
}
|
||||
ButtonSize.Large -> {
|
||||
when (style) {
|
||||
ButtonStyle.Text -> PaddingValues(horizontal = 16.dp, vertical = 13.dp)
|
||||
else -> PaddingValues(horizontal = 24.dp, vertical = 13.dp)
|
||||
}
|
||||
ButtonSize.Large -> when (style) {
|
||||
ButtonStyle.Filled,
|
||||
ButtonStyle.Outlined -> if (hasStartDrawable)
|
||||
PaddingValues(start = 24.dp, top = 13.dp, end = 32.dp, bottom = 13.dp)
|
||||
else
|
||||
PaddingValues(start = 32.dp, top = 13.dp, end = 32.dp, bottom = 13.dp)
|
||||
ButtonStyle.Text -> if (hasStartDrawable)
|
||||
PaddingValues(start = 12.dp, top = 13.dp, end = 16.dp, bottom = 13.dp)
|
||||
else
|
||||
PaddingValues(start = 16.dp, top = 13.dp, end = 16.dp, bottom = 13.dp)
|
||||
}
|
||||
}
|
||||
|
||||
val shape = when (style) {
|
||||
ButtonStyle.Filled, ButtonStyle.Outlined -> RoundedCornerShape(percent = 50)
|
||||
ButtonStyle.Filled,
|
||||
ButtonStyle.Outlined -> RoundedCornerShape(percent = 50)
|
||||
ButtonStyle.Text -> RectangleShape
|
||||
}
|
||||
|
||||
val border = when (style) {
|
||||
ButtonStyle.Filled, ButtonStyle.Text -> null
|
||||
ButtonStyle.Filled -> null
|
||||
ButtonStyle.Outlined -> BorderStroke(
|
||||
width = 1.dp,
|
||||
color = ElementTheme.colors.borderInteractiveSecondary
|
||||
)
|
||||
ButtonStyle.Text -> null
|
||||
}
|
||||
|
||||
val textStyle = when (size) {
|
||||
|
|
@ -166,11 +181,6 @@ internal fun ButtonInternal(
|
|||
ButtonSize.Large -> ElementTheme.typography.fontBodyLgMedium
|
||||
}
|
||||
|
||||
val internalPadding = when {
|
||||
style == ButtonStyle.Text -> if (leadingIcon != null) PaddingValues(start = 8.dp) else PaddingValues(0.dp)
|
||||
else -> PaddingValues(horizontal = 8.dp)
|
||||
}
|
||||
|
||||
androidx.compose.material3.Button(
|
||||
onClick = {
|
||||
if (!showProgress) {
|
||||
|
|
@ -195,6 +205,7 @@ internal fun ButtonInternal(
|
|||
color = LocalContentColor.current,
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
leadingIcon != null -> {
|
||||
androidx.compose.material.Icon(
|
||||
|
|
@ -203,15 +214,14 @@ internal fun ButtonInternal(
|
|||
tint = LocalContentColor.current,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = textStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(internalPadding),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,15 @@ interface MatrixClient : Closeable {
|
|||
* Will close the client and delete the cache data.
|
||||
*/
|
||||
suspend fun clearCache()
|
||||
suspend fun logout()
|
||||
|
||||
/**
|
||||
* Logout the user.
|
||||
* Returns an optional URL. When the URL is there, it should be presented to the user after logout for RP initiated logout on their account page.
|
||||
*/
|
||||
suspend fun logout(): String?
|
||||
suspend fun loadUserDisplayName(): Result<String>
|
||||
suspend fun loadUserAvatarURLString(): Result<String?>
|
||||
suspend fun getAccountManagementUrl(): Result<String?>
|
||||
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
||||
fun roomMembershipObserver(): RoomMembershipObserver
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,5 @@ sealed class AuthenticationException(message: String) : Exception(message) {
|
|||
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
|
||||
class SessionMissing(message: String) : AuthenticationException(message)
|
||||
class Generic(message: String) : AuthenticationException(message)
|
||||
// TODO Oidc
|
||||
// class OidcError(type: String, message: String) : AuthenticationException(message)
|
||||
data class OidcError(val type: String, override val message: String) : AuthenticationException(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
|||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -141,6 +142,21 @@ interface MatrixRoom : Closeable {
|
|||
assetType: AssetType? = null,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Create a poll in the room.
|
||||
*
|
||||
* @param question The question to ask.
|
||||
* @param answers The list of answers.
|
||||
* @param maxSelections The maximum number of answers that can be selected.
|
||||
* @param pollKind The kind of poll to create.
|
||||
*/
|
||||
suspend fun createPoll(
|
||||
question: String,
|
||||
answers: List<String>,
|
||||
maxSelections: Int,
|
||||
pollKind: PollKind,
|
||||
): Result<Unit>
|
||||
|
||||
override fun close() = destroy()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.verification
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface SessionVerificationService {
|
||||
|
|
@ -37,6 +38,11 @@ interface SessionVerificationService {
|
|||
*/
|
||||
val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus>
|
||||
|
||||
/**
|
||||
* Returns whether the current session needs to be verified and the SDK is ready to start the verification.
|
||||
*/
|
||||
val canVerifySessionFlow: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Request verification of the current session.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
|||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||
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.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.pushers.RustPushersService
|
||||
|
|
@ -92,8 +92,8 @@ class RustMatrixClient constructor(
|
|||
private val innerRoomListService = syncService.roomListService()
|
||||
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
||||
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
|
||||
private val verificationService = RustSessionVerificationService()
|
||||
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
|
||||
private val verificationService = RustSessionVerificationService(rustSyncService)
|
||||
private val pushersService = RustPushersService(
|
||||
client = client,
|
||||
dispatchers = dispatchers,
|
||||
|
|
@ -119,6 +119,13 @@ class RustMatrixClient constructor(
|
|||
Timber.v("didReceiveAuthError -> already cleaning up")
|
||||
}
|
||||
}
|
||||
|
||||
override fun didRefreshTokens() {
|
||||
Timber.w("didRefreshTokens()")
|
||||
appCoroutineScope.launch {
|
||||
sessionStore.updateData(client.session().toSessionData())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val rustRoomListService: RoomListService =
|
||||
|
|
@ -141,13 +148,11 @@ class RustMatrixClient constructor(
|
|||
|
||||
init {
|
||||
client.setDelegate(clientDelegate)
|
||||
rustSyncService.syncState
|
||||
.onEach { syncState ->
|
||||
if (syncState == SyncState.Running) {
|
||||
onSlidingSyncUpdate()
|
||||
}
|
||||
roomListService.state.onEach { state ->
|
||||
if (state == RoomListService.State.Running) {
|
||||
setupVerificationControllerIfNeeded()
|
||||
}
|
||||
.launchIn(sessionCoroutineScope)
|
||||
}.launchIn(sessionCoroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) {
|
||||
|
|
@ -287,21 +292,30 @@ class RustMatrixClient constructor(
|
|||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
|
||||
}
|
||||
|
||||
override suspend fun logout() = doLogout(doRequest = true)
|
||||
override suspend fun logout(): String? = doLogout(doRequest = true)
|
||||
|
||||
private suspend fun doLogout(doRequest: Boolean) = withContext(sessionDispatcher) {
|
||||
if (doRequest) {
|
||||
try {
|
||||
client.logout()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
||||
private suspend fun doLogout(doRequest: Boolean): String? {
|
||||
var result: String? = null
|
||||
withContext(sessionDispatcher) {
|
||||
if (doRequest) {
|
||||
try {
|
||||
result = client.logout()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
||||
}
|
||||
}
|
||||
close()
|
||||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
|
||||
sessionStore.removeSession(sessionId.value)
|
||||
}
|
||||
close()
|
||||
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = true)
|
||||
sessionStore.removeSession(sessionId.value)
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun getAccountManagementUrl(): Result<String?> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
client.accountUrl()
|
||||
}
|
||||
}
|
||||
override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
client.displayName()
|
||||
|
|
@ -321,8 +335,8 @@ class RustMatrixClient constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onSlidingSyncUpdate() {
|
||||
if (!verificationService.isReady.value) {
|
||||
private fun setupVerificationControllerIfNeeded() {
|
||||
if (verificationService.verificationController == null) {
|
||||
try {
|
||||
verificationService.verificationController = client.getSessionVerificationController()
|
||||
} catch (e: Throwable) {
|
||||
|
|
|
|||
|
|
@ -75,4 +75,5 @@ private fun SessionData.toSession() = Session(
|
|||
deviceId = deviceId,
|
||||
homeserverUrl = homeserverUrl,
|
||||
slidingSyncProxy = slidingSyncProxy,
|
||||
oidcData = oidcData,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,15 +26,12 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
|||
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
|
||||
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
|
||||
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
|
||||
|
||||
/* TODO Oidc
|
||||
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
|
||||
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
|
||||
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
|
||||
is RustAuthenticationException.OidcNotStarted -> AuthenticationException.OidcError("OidcNotStarted", message!!)
|
||||
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
|
||||
*/
|
||||
|
||||
is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message!!)
|
||||
is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message!!)
|
||||
else -> AuthenticationException.Generic(this.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
|
|||
MatrixHomeServerDetails(
|
||||
url = url(),
|
||||
supportsPasswordLogin = supportsPasswordLogin(),
|
||||
supportsOidcLogin = false // TODO Oidc supportsOidcLogin(),
|
||||
supportsOidcLogin = supportsOidcLogin(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,19 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.auth
|
||||
|
||||
// TODO Oidc
|
||||
// import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||
// import org.matrix.rustcomponents.sdk.OidcClientMetadata
|
||||
import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||
import org.matrix.rustcomponents.sdk.OidcConfiguration
|
||||
|
||||
/*
|
||||
val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata(
|
||||
val oidcConfiguration: OidcConfiguration = OidcConfiguration(
|
||||
clientName = "Element",
|
||||
redirectUri = OidcConfig.redirectUri,
|
||||
clientUri = "https://element.io",
|
||||
tosUri = "https://element.io/user-terms-of-service",
|
||||
policyUri = "https://element.io/privacy"
|
||||
policyUri = "https://element.io/privacy",
|
||||
/**
|
||||
* Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
|
||||
*/
|
||||
staticRegistrations = mapOf(
|
||||
"https://id.thirdroom.io/realms/thirdroom" to "elementx",
|
||||
),
|
||||
)
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.auth
|
||||
|
||||
// TODO Oidc
|
||||
// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
|
|
@ -30,17 +28,16 @@ import io.element.android.libraries.matrix.api.auth.OidcDetails
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
|
||||
|
||||
|
|
@ -57,9 +54,8 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
private val authService: RustAuthenticationService = RustAuthenticationService(
|
||||
basePath = baseDirectory.absolutePath,
|
||||
passphrase = null,
|
||||
// TODO Oidc
|
||||
// oidcClientMetadata = oidcClientMetadata,
|
||||
userAgent = userAgentProvider.provide(),
|
||||
oidcConfiguration = oidcConfiguration,
|
||||
customSlidingSyncProxy = null,
|
||||
)
|
||||
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
|
||||
|
|
@ -112,68 +108,48 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO Oidc
|
||||
// private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null
|
||||
private var pendingOidcAuthenticationData: OidcAuthenticationData? = null
|
||||
|
||||
override suspend fun getOidcUrl(): Result<OidcDetails> {
|
||||
TODO("Oidc")
|
||||
/*
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val urlForOidcLogin = authService.urlForOidcLogin()
|
||||
val url = urlForOidcLogin.loginUrl()
|
||||
pendingUrlForOidcLogin = urlForOidcLogin
|
||||
val oidcAuthenticationData = authService.urlForOidcLogin()
|
||||
val url = oidcAuthenticationData.loginUrl()
|
||||
pendingOidcAuthenticationData = oidcAuthenticationData
|
||||
OidcDetails(url)
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
override suspend fun cancelOidcLogin(): Result<Unit> {
|
||||
TODO("Oidc")
|
||||
/*
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
pendingUrlForOidcLogin?.close()
|
||||
pendingUrlForOidcLogin = null
|
||||
pendingOidcAuthenticationData?.close()
|
||||
pendingOidcAuthenticationData = null
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters).
|
||||
*/
|
||||
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
|
||||
TODO("Oidc")
|
||||
/*
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val urlForOidcLogin = pendingUrlForOidcLogin ?: error("You need to call `getOidcUrl()` first")
|
||||
val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first")
|
||||
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
|
||||
val sessionData = client.use { it.session().toSessionData() }
|
||||
pendingUrlForOidcLogin = null
|
||||
pendingOidcAuthenticationData?.close()
|
||||
pendingOidcAuthenticationData = null
|
||||
sessionStore.storeData(sessionData)
|
||||
SessionId(sessionData.userId)
|
||||
}.mapFailure { failure ->
|
||||
failure.mapAuthenticationException()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun Session.toSessionData() = SessionData(
|
||||
userId = userId,
|
||||
deviceId = deviceId,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
homeserverUrl = homeserverUrl,
|
||||
slidingSyncProxy = slidingSyncProxy,
|
||||
loginTimestamp = Date(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.mapper
|
||||
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
import java.util.Date
|
||||
|
||||
internal fun Session.toSessionData() = SessionData(
|
||||
userId = userId,
|
||||
deviceId = deviceId,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
homeserverUrl = homeserverUrl,
|
||||
oidcData = oidcData,
|
||||
slidingSyncProxy = slidingSyncProxy,
|
||||
loginTimestamp = Date(),
|
||||
)
|
||||
|
|
@ -23,3 +23,8 @@ fun RustPollKind.map(): PollKind = when (this) {
|
|||
RustPollKind.DISCLOSED -> PollKind.Disclosed
|
||||
RustPollKind.UNDISCLOSED -> PollKind.Undisclosed
|
||||
}
|
||||
|
||||
fun PollKind.toInner(): RustPollKind = when (this) {
|
||||
PollKind.Disclosed -> RustPollKind.DISCLOSED
|
||||
PollKind.Undisclosed -> RustPollKind.UNDISCLOSED
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
|||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
|
|
@ -41,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
|||
import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
||||
import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl
|
||||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.poll.toInner
|
||||
import io.element.android.libraries.matrix.impl.room.location.toInner
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
||||
|
|
@ -378,7 +380,24 @@ class RustMatrixRoom(
|
|||
description = description,
|
||||
zoomLevel = zoomLevel?.toUByte(),
|
||||
assetType = assetType?.toInner(),
|
||||
txnId = genTransactionId()
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createPoll(
|
||||
question: String,
|
||||
answers: List<String>,
|
||||
maxSelections: Int,
|
||||
pollKind: PollKind,
|
||||
): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.createPoll(
|
||||
question = question,
|
||||
answers = answers,
|
||||
maxSelections = maxSelections.toUByte(),
|
||||
pollKind = pollKind.toInner(),
|
||||
txnId = genTransactionId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class RoomMessageFactory {
|
|||
eventTimelineItem ?: return null
|
||||
val mappedTimelineItem = EventTimelineItemMapper().map(eventTimelineItem)
|
||||
return RoomMessage(
|
||||
eventId = mappedTimelineItem.eventId!!,
|
||||
eventId = mappedTimelineItem.eventId ?: return null,
|
||||
event = mappedTimelineItem,
|
||||
sender = mappedTimelineItem.sender,
|
||||
originServerTs = mappedTimelineItem.timestamp,
|
||||
|
|
|
|||
|
|
@ -17,20 +17,25 @@
|
|||
package io.element.android.libraries.matrix.impl.verification
|
||||
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationController
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationEmoji
|
||||
import javax.inject.Inject
|
||||
|
||||
class RustSessionVerificationService @Inject constructor() : SessionVerificationService, SessionVerificationControllerDelegate {
|
||||
class RustSessionVerificationService @Inject constructor(
|
||||
private val syncService: RustSyncService,
|
||||
) : SessionVerificationService, SessionVerificationControllerDelegate {
|
||||
|
||||
var verificationController: SessionVerificationControllerInterface? = null
|
||||
set(value) {
|
||||
|
|
@ -52,6 +57,10 @@ class RustSessionVerificationService @Inject constructor() : SessionVerification
|
|||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
|
||||
|
||||
override val canVerifySessionFlow = combine(sessionVerifiedStatus, syncService.syncState) { verificationStatus, syncState ->
|
||||
syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified
|
||||
}
|
||||
|
||||
override suspend fun requestVerification() = tryOrFail {
|
||||
verificationController?.requestVerification()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class FakeMatrixClient(
|
|||
private val pushersService: FakePushersService = FakePushersService(),
|
||||
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
||||
private val syncService: FakeSyncService = FakeSyncService(),
|
||||
private val accountManagementUrlString: Result<String?> = Result.success(null),
|
||||
) : MatrixClient {
|
||||
|
||||
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
|
||||
|
|
@ -109,9 +110,10 @@ class FakeMatrixClient(
|
|||
override suspend fun clearCache() {
|
||||
}
|
||||
|
||||
override suspend fun logout() {
|
||||
override suspend fun logout(): String? {
|
||||
delay(100)
|
||||
logoutFailure?.let { throw it }
|
||||
return null
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
|
|
@ -124,6 +126,9 @@ class FakeMatrixClient(
|
|||
return userAvatarURLString
|
||||
}
|
||||
|
||||
override suspend fun getAccountManagementUrl(): Result<String?> {
|
||||
return accountManagementUrlString
|
||||
}
|
||||
override suspend fun uploadMedia(
|
||||
mimeType: String,
|
||||
data: ByteArray,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
|||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
|
|
@ -83,6 +84,7 @@ class FakeMatrixRoom(
|
|||
private var forwardEventResult = Result.success(Unit)
|
||||
private var reportContentResult = Result.success(Unit)
|
||||
private var sendLocationResult = Result.success(Unit)
|
||||
private var createPollResult = Result.success(Unit)
|
||||
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
||||
val editMessageCalls = mutableListOf<String>()
|
||||
|
||||
|
|
@ -104,6 +106,9 @@ class FakeMatrixRoom(
|
|||
private val _sentLocations = mutableListOf<SendLocationInvocation>()
|
||||
val sentLocations: List<SendLocationInvocation> = _sentLocations
|
||||
|
||||
private val _createPollInvocations = mutableListOf<CreatePollInvocation>()
|
||||
val createPollInvocations: List<CreatePollInvocation> = _createPollInvocations
|
||||
|
||||
var invitedUserId: UserId? = null
|
||||
private set
|
||||
|
||||
|
|
@ -305,6 +310,16 @@ class FakeMatrixRoom(
|
|||
return sendLocationResult
|
||||
}
|
||||
|
||||
override suspend fun createPoll(
|
||||
question: String,
|
||||
answers: List<String>,
|
||||
maxSelections: Int,
|
||||
pollKind: PollKind
|
||||
): Result<Unit> = simulateLongTask {
|
||||
_createPollInvocations.add(CreatePollInvocation(question, answers, maxSelections, pollKind))
|
||||
return createPollResult
|
||||
}
|
||||
|
||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||
this.leaveRoomError = throwable
|
||||
}
|
||||
|
|
@ -397,6 +412,10 @@ class FakeMatrixRoom(
|
|||
sendLocationResult = result
|
||||
}
|
||||
|
||||
fun givenCreatePollResult(result: Result<Unit>) {
|
||||
createPollResult = result
|
||||
}
|
||||
|
||||
fun givenProgressCallbackValues(values: List<Pair<Long, Long>>) {
|
||||
progressCallbackValues = values
|
||||
}
|
||||
|
|
@ -409,3 +428,10 @@ data class SendLocationInvocation(
|
|||
val zoomLevel: Int?,
|
||||
val assetType: AssetType?,
|
||||
)
|
||||
|
||||
data class CreatePollInvocation(
|
||||
val question: String,
|
||||
val answers: List<String>,
|
||||
val maxSelections: Int,
|
||||
val pollKind: PollKind,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS
|
|||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
|
|
@ -27,13 +28,13 @@ class FakeSessionVerificationService : SessionVerificationService {
|
|||
private val _isReady = MutableStateFlow(false)
|
||||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||
private var _canVerifySessionFlow = MutableStateFlow(true)
|
||||
private var emojiList = emptyList<VerificationEmoji>()
|
||||
var shouldFail = false
|
||||
|
||||
override val verificationFlowState: StateFlow<VerificationFlowState>
|
||||
get() = _verificationFlowState
|
||||
|
||||
override val verificationFlowState: StateFlow<VerificationFlowState> =_verificationFlowState
|
||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
|
||||
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
|
||||
|
||||
override val isReady: StateFlow<Boolean> = _isReady
|
||||
|
||||
|
|
@ -77,6 +78,10 @@ class FakeSessionVerificationService : SessionVerificationService {
|
|||
_verificationFlowState.value = state
|
||||
}
|
||||
|
||||
fun givenCanVerifySession(canVerify: Boolean) {
|
||||
_canVerifySessionFlow.value = canVerify
|
||||
}
|
||||
|
||||
fun givenIsReady(value: Boolean) {
|
||||
_isReady.value = value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import com.otaliastudios.transcoder.Transcoder
|
||||
import com.otaliastudios.transcoder.TranscoderListener
|
||||
import com.otaliastudios.transcoder.resize.AtMostResizer
|
||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||
import io.element.android.libraries.androidutils.file.createTmpFile
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
|
|
@ -35,6 +37,11 @@ class VideoCompressor @Inject constructor(
|
|||
fun compress(uri: Uri) = callbackFlow {
|
||||
val tmpFile = context.createTmpFile(extension = "mp4")
|
||||
val future = Transcoder.into(tmpFile.path)
|
||||
.setVideoTrackStrategy(
|
||||
DefaultVideoStrategy.Builder()
|
||||
.addResizer(AtMostResizer(1920, 1080))
|
||||
.build()
|
||||
)
|
||||
.addDataSource(context, uri)
|
||||
.setListener(object : TranscoderListener {
|
||||
override fun onTranscodeProgress(progress: Double) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ data class SessionData(
|
|||
val accessToken: String,
|
||||
val refreshToken: String?,
|
||||
val homeserverUrl: String,
|
||||
val oidcData: String?,
|
||||
val slidingSyncProxy: String?,
|
||||
val loginTimestamp: Date?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ interface SessionStore {
|
|||
fun isLoggedIn(): Flow<Boolean>
|
||||
fun sessionsFlow(): Flow<List<SessionData>>
|
||||
suspend fun storeData(sessionData: SessionData)
|
||||
|
||||
/**
|
||||
* Will update the session data matching the userId, except the value of loginTimestamp.
|
||||
* No op if userId is not found in DB.
|
||||
*/
|
||||
suspend fun updateData(sessionData: SessionData)
|
||||
suspend fun getSession(sessionId: String): SessionData?
|
||||
suspend fun getAllSessions(): List<SessionData>
|
||||
suspend fun getLatestSession(): SessionData?
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ class InMemorySessionStore : SessionStore {
|
|||
sessionDataFlow.value = sessionData
|
||||
}
|
||||
|
||||
override suspend fun updateData(sessionData: SessionData) {
|
||||
sessionDataFlow.value = sessionData
|
||||
}
|
||||
|
||||
override suspend fun getSession(sessionId: String): SessionData? {
|
||||
return sessionDataFlow.value.takeIf { it?.userId == sessionId }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,24 @@ class DatabaseSessionStore @Inject constructor(
|
|||
database.sessionDataQueries.insertSessionData(sessionData.toDbModel())
|
||||
}
|
||||
|
||||
override suspend fun updateData(sessionData: SessionData) {
|
||||
val result = database.sessionDataQueries.selectByUserId(sessionData.userId)
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
|
||||
if (result == null) {
|
||||
Timber.e("User ${sessionData.userId} not found in session database")
|
||||
return
|
||||
}
|
||||
|
||||
// Copy new data from SDK, but keep login timestamp
|
||||
database.sessionDataQueries.updateSession(
|
||||
sessionData.copy(
|
||||
loginTimestamp = result.loginTimestamp,
|
||||
).toDbModel()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getLatestSession(): SessionData? {
|
||||
return database.sessionDataQueries.selectFirst()
|
||||
.executeAsOneOrNull()
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
|||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
homeserverUrl = homeserverUrl,
|
||||
oidcData = oidcData,
|
||||
slidingSyncProxy = slidingSyncProxy,
|
||||
loginTimestamp = loginTimestamp?.time,
|
||||
)
|
||||
|
|
@ -39,6 +40,7 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
|||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
homeserverUrl = homeserverUrl,
|
||||
oidcData = oidcData,
|
||||
slidingSyncProxy = slidingSyncProxy,
|
||||
loginTimestamp = loginTimestamp?.let { Date(it) }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ CREATE TABLE SessionData (
|
|||
refreshToken TEXT,
|
||||
homeserverUrl TEXT NOT NULL,
|
||||
slidingSyncProxy TEXT,
|
||||
loginTimestamp INTEGER
|
||||
loginTimestamp INTEGER,
|
||||
oidcData TEXT
|
||||
);
|
||||
|
||||
|
||||
|
|
@ -23,3 +24,6 @@ INSERT INTO SessionData VALUES ?;
|
|||
|
||||
removeSession:
|
||||
DELETE FROM SessionData WHERE userId = ?;
|
||||
|
||||
updateSession:
|
||||
REPLACE INTO SessionData VALUES ?;
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE SessionData ADD COLUMN oidcData TEXT;
|
||||
|
|
@ -37,6 +37,7 @@ class DatabaseSessionStoreTests {
|
|||
homeserverUrl = "homeserverUrl",
|
||||
slidingSyncProxy = null,
|
||||
loginTimestamp = null,
|
||||
oidcData = "aOidcData",
|
||||
)
|
||||
|
||||
@Before
|
||||
|
|
@ -108,4 +109,45 @@ class DatabaseSessionStoreTests {
|
|||
|
||||
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update session update all fields except loginTimestamp`() = runTest {
|
||||
val firstSessionData = SessionData(
|
||||
userId = "userId",
|
||||
deviceId = "deviceId",
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
homeserverUrl = "homeserverUrl",
|
||||
slidingSyncProxy = "slidingSyncProxy",
|
||||
loginTimestamp = 1,
|
||||
oidcData = "aOidcData",
|
||||
)
|
||||
val secondSessionData = SessionData(
|
||||
userId = "userId",
|
||||
deviceId = "deviceIdAltered",
|
||||
accessToken = "accessTokenAltered",
|
||||
refreshToken = "refreshTokenAltered",
|
||||
homeserverUrl = "homeserverUrlAltered",
|
||||
slidingSyncProxy = "slidingSyncProxyAltered",
|
||||
loginTimestamp = 2,
|
||||
oidcData = "aOidcDataAltered",
|
||||
)
|
||||
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
||||
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
||||
|
||||
database.sessionDataQueries.insertSessionData(firstSessionData)
|
||||
databaseSessionStore.updateData(secondSessionData.toApiModel())
|
||||
|
||||
// Get the altered session
|
||||
val alteredSession = databaseSessionStore.getSession(firstSessionData.userId)!!.toDbModel()
|
||||
|
||||
assertThat(alteredSession.userId).isEqualTo(secondSessionData.userId)
|
||||
assertThat(alteredSession.deviceId).isEqualTo(secondSessionData.deviceId)
|
||||
assertThat(alteredSession.accessToken).isEqualTo(secondSessionData.accessToken)
|
||||
assertThat(alteredSession.refreshToken).isEqualTo(secondSessionData.refreshToken)
|
||||
assertThat(alteredSession.homeserverUrl).isEqualTo(secondSessionData.homeserverUrl)
|
||||
assertThat(alteredSession.slidingSyncProxy).isEqualTo(secondSessionData.slidingSyncProxy)
|
||||
assertThat(alteredSession.loginTimestamp).isEqualTo(/* Not altered! */ firstSessionData.loginTimestamp)
|
||||
assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,11 +180,21 @@
|
|||
<string name="screen_migration_title">"Setting up your account."</string>
|
||||
<string name="screen_notification_settings_additional_settings_section_title">"Additional settings"</string>
|
||||
<string name="screen_notification_settings_calls_label">"Audio and video calls"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch">"Configuration mismatch"</string>
|
||||
<string name="screen_notification_settings_configuration_mismatch_description">"We’ve simplified Notifications Settings to make options easier to find.
|
||||
|
||||
Some custom settings you’ve chosen in the past are not shown here, but they’re still active.
|
||||
|
||||
If you proceed, some of your settings may change."</string>
|
||||
<string name="screen_notification_settings_direct_chats">"Direct chats"</string>
|
||||
<string name="screen_notification_settings_edit_custom_settings_section_title">"Custom setting per chat"</string>
|
||||
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
|
||||
<string name="screen_notification_settings_edit_mode_all_messages">"All messages"</string>
|
||||
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
||||
<string name="screen_notification_settings_edit_screen_direct_section_header">"On direct chats, notify me for"</string>
|
||||
<string name="screen_notification_settings_edit_screen_group_section_header">"On group chats, notify me for"</string>
|
||||
<string name="screen_notification_settings_enable_notifications">"Enable notifications on this device"</string>
|
||||
<string name="screen_notification_settings_failed_fixing_configuration">"The configuration has not been corrected, please try again."</string>
|
||||
<string name="screen_notification_settings_group_chats">"Group chats"</string>
|
||||
<string name="screen_notification_settings_mentions_section_title">"Mentions"</string>
|
||||
<string name="screen_notification_settings_mode_all">"All"</string>
|
||||
|
|
@ -196,6 +206,7 @@
|
|||
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
|
||||
<string name="screen_notification_settings_title">"Notifications"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
|
||||
<string name="screen_settings_oidc_account">"Account and devices"</string>
|
||||
<string name="screen_share_location_title">"Share location"</string>
|
||||
<string name="screen_share_my_location_action">"Share my location"</string>
|
||||
<string name="screen_share_open_apple_maps">"Open in Apple Maps"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue