Merge branch 'develop' of github.com:element-hq/element-x-android into align-cta-button-on-login-flow
# Conflicts: # features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_0,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_1,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_2,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_3,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Day-0_1_null_4,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_0,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_1,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_2,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_3,NEXUS_5,1.0,en].png # tests/uitests/src/test/snapshots/images/ui_S_t[f.onboarding.impl_OnBoardingScreen_null_OnBoardingScreen-Night-0_2_null_4,NEXUS_5,1.0,en].png
This commit is contained in:
commit
f98cd5b99b
694 changed files with 6806 additions and 1630 deletions
|
|
@ -25,7 +25,6 @@ data class BuildMeta(
|
|||
val versionName: String,
|
||||
val versionCode: Int,
|
||||
val gitRevision: String,
|
||||
val gitRevisionDate: String,
|
||||
val gitBranchName: String,
|
||||
val flavorDescription: String,
|
||||
val flavorShortDescription: String,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package io.element.android.libraries.designsystem.components.async
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
|
|
@ -67,8 +69,9 @@ fun <T> AsyncActionView(
|
|||
}
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
val latestOnSuccess by rememberUpdatedState(onSuccess)
|
||||
LaunchedEffect(async) {
|
||||
onSuccess(async.data)
|
||||
latestOnSuccess(async.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.components.tooltip
|
||||
|
||||
import androidx.compose.material3.CaretScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -27,7 +28,7 @@ import androidx.compose.material3.PlainTooltip as M3PlainTooltip
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PlainTooltip(
|
||||
fun CaretScope.PlainTooltip(
|
||||
modifier: Modifier = Modifier,
|
||||
contentColor: Color = ElementTheme.colors.textOnSolidPrimary,
|
||||
containerColor: Color = ElementTheme.colors.bgActionPrimaryRest,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.components.tooltip
|
||||
|
||||
import androidx.compose.material3.CaretScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -27,7 +28,7 @@ import androidx.compose.material3.TooltipBox as M3TooltipBox
|
|||
@Composable
|
||||
fun TooltipBox(
|
||||
positionProvider: PopupPositionProvider,
|
||||
tooltip: @Composable () -> Unit,
|
||||
tooltip: @Composable CaretScope.() -> Unit,
|
||||
state: TooltipState,
|
||||
modifier: Modifier = Modifier,
|
||||
focusable: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.CheckboxColors
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -57,7 +58,7 @@ fun Checkbox(
|
|||
onCheckedChange(!checked)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
modifier = modifier.minimumInteractiveComponentSize(),
|
||||
enabled = enabled,
|
||||
colors = colors,
|
||||
interactionSource = interactionSource,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.RadioButtonColors
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -48,7 +49,7 @@ fun RadioButton(
|
|||
androidx.compose.material3.RadioButton(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
modifier = modifier.minimumInteractiveComponentSize(),
|
||||
enabled = enabled,
|
||||
colors = colors,
|
||||
interactionSource = interactionSource,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SwitchColors
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -52,7 +53,7 @@ fun Switch(
|
|||
Material3Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
modifier = modifier,
|
||||
modifier = modifier.minimumInteractiveComponentSize(),
|
||||
enabled = enabled,
|
||||
colors = colors,
|
||||
interactionSource = interactionSource,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.di.annotations
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
/**
|
||||
* Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for an active session.
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@Qualifier
|
||||
annotation class SessionCoroutineScope
|
||||
|
|
@ -69,6 +69,8 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
|||
|
||||
override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? {
|
||||
val isOutgoing = event.isOwn
|
||||
// Note: we do not use disambiguated display name here, see
|
||||
// https://github.com/element-hq/element-x-ios/issues/1845#issuecomment-1888707428
|
||||
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
|
||||
return when (val content = event.content) {
|
||||
is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom)
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
|
@ -48,7 +48,7 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
|||
) : TimelineEventFormatter {
|
||||
override fun format(event: EventTimelineItem): CharSequence? {
|
||||
val isOutgoing = event.isOwn
|
||||
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
|
||||
val senderDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
return when (val content = event.content) {
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@
|
|||
<string name="state_event_room_name_changed_by_you">"Hai cambiato il nome della stanza in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s ha rimosso il nome della stanza"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Hai rimosso il nome della stanza"</string>
|
||||
<string name="state_event_room_none">"%1$s non ha apportato modifiche"</string>
|
||||
<string name="state_event_room_none_by_you">"Non hai apportato modifiche"</string>
|
||||
<string name="state_event_room_reject">"%1$s ha rifiutato l\'invito"</string>
|
||||
<string name="state_event_room_reject_by_you">"Hai rifiutato l\'invito"</string>
|
||||
<string name="state_event_room_remove">"%1$s ha rimosso %2$s"</string>
|
||||
|
|
|
|||
|
|
@ -55,5 +55,5 @@
|
|||
<string name="state_event_room_topic_removed_by_you">"Вы удалили тему комнаты"</string>
|
||||
<string name="state_event_room_unban">"%1$s разблокирован %2$s"</string>
|
||||
<string name="state_event_room_unban_by_you">"Вы разблокировали %1$s"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s внес неизвестное изменение в составе"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s внес неизвестное изменение для своих участников"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -46,4 +46,5 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,15 @@ import io.element.android.libraries.matrix.api.sync.SyncService
|
|||
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 kotlinx.coroutines.CoroutineScope
|
||||
import java.io.Closeable
|
||||
|
||||
interface MatrixClient : Closeable {
|
||||
val sessionId: SessionId
|
||||
val deviceId: String
|
||||
val roomListService: RoomListService
|
||||
val mediaLoader: MatrixMediaLoader
|
||||
val sessionCoroutineScope: CoroutineScope
|
||||
suspend fun getRoom(roomId: RoomId): MatrixRoom?
|
||||
suspend fun findDM(userId: UserId): RoomId?
|
||||
suspend fun ignoreUser(userId: UserId): Result<Unit>
|
||||
|
|
@ -72,7 +75,7 @@ interface MatrixClient : Closeable {
|
|||
*/
|
||||
suspend fun logout(ignoreSdkError: Boolean): String?
|
||||
suspend fun loadUserDisplayName(): Result<String>
|
||||
suspend fun loadUserAvatarURLString(): Result<String?>
|
||||
suspend fun loadUserAvatarUrl(): Result<String?>
|
||||
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
|
||||
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
||||
fun roomMembershipObserver(): RoomMembershipObserver
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ data class NotificationData(
|
|||
val roomId: RoomId,
|
||||
// mxc url
|
||||
val senderAvatarUrl: String?,
|
||||
val senderDisplayName: String?,
|
||||
// private, must use `getSenderName`
|
||||
private val senderDisplayName: String?,
|
||||
private val senderIsNameAmbiguous: Boolean,
|
||||
val roomAvatarUrl: String?,
|
||||
val roomDisplayName: String?,
|
||||
val isDirect: Boolean,
|
||||
|
|
@ -36,7 +38,13 @@ data class NotificationData(
|
|||
val timestamp: Long,
|
||||
val content: NotificationContent,
|
||||
val hasMention: Boolean,
|
||||
)
|
||||
) {
|
||||
fun getSenderName(userId: UserId): String = when {
|
||||
senderDisplayName.isNullOrBlank() -> userId.value
|
||||
senderIsNameAmbiguous -> "$senderDisplayName ($userId)"
|
||||
else -> senderDisplayName
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface NotificationContent {
|
||||
sealed interface MessageLike : NotificationContent {
|
||||
|
|
@ -54,11 +62,13 @@ sealed interface NotificationContent {
|
|||
data class ReactionContent(
|
||||
val relatedEventId: String
|
||||
) : MessageLike
|
||||
|
||||
data object RoomEncrypted : MessageLike
|
||||
data class RoomMessage(
|
||||
val senderId: UserId,
|
||||
val messageType: MessageType
|
||||
) : MessageLike
|
||||
|
||||
data object RoomRedaction : MessageLike
|
||||
data object Sticker : MessageLike
|
||||
data class Poll(
|
||||
|
|
@ -83,6 +93,7 @@ sealed interface NotificationContent {
|
|||
val userId: String,
|
||||
val membershipState: RoomMembershipState
|
||||
) : StateEvent
|
||||
|
||||
data object RoomName : StateEvent
|
||||
data object RoomPinnedEvents : StateEvent
|
||||
data object RoomPowerLevels : StateEvent
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ interface MatrixRoom : Closeable {
|
|||
val activeMemberCount: Long
|
||||
val joinedMemberCount: Long
|
||||
|
||||
/** Whether the room is a direct message. */
|
||||
val isDm: Boolean get() = isDirect && isOneToOne
|
||||
|
||||
val roomInfoFlow: Flow<MatrixRoomInfo>
|
||||
|
||||
/**
|
||||
|
|
@ -72,7 +75,7 @@ interface MatrixRoom : Closeable {
|
|||
/**
|
||||
* Try to load the room members and update the membersFlow.
|
||||
*/
|
||||
suspend fun updateMembers(): Result<Unit>
|
||||
suspend fun updateMembers()
|
||||
|
||||
suspend fun updateRoomNotificationSettings(): Result<Unit>
|
||||
|
||||
|
|
@ -124,7 +127,9 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun canUserInvite(userId: UserId): Result<Boolean>
|
||||
|
||||
suspend fun canUserRedact(userId: UserId): Result<Boolean>
|
||||
suspend fun canUserRedactOwn(userId: UserId): Result<Boolean>
|
||||
|
||||
suspend fun canUserRedactOther(userId: UserId): Result<Boolean>
|
||||
|
||||
suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean>
|
||||
|
||||
|
|
@ -219,6 +224,12 @@ interface MatrixRoom : Closeable {
|
|||
progressCallback: ProgressCallback?
|
||||
): Result<MediaUploadHandler>
|
||||
|
||||
/**
|
||||
* Send a typing notification.
|
||||
* @param isTyping True if the user is typing, false otherwise.
|
||||
*/
|
||||
suspend fun typingNotice(isTyping: Boolean): Result<Unit>
|
||||
|
||||
/**
|
||||
* Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters.
|
||||
* @param widgetSettings The widget settings to use.
|
||||
|
|
@ -241,7 +252,5 @@ interface MatrixRoom : Closeable {
|
|||
*/
|
||||
fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver>
|
||||
|
||||
fun pollHistory(): MatrixTimeline
|
||||
|
||||
override fun close() = destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ suspend fun MatrixRoom.canSendState(type: StateEventType): Result<Boolean> = can
|
|||
suspend fun MatrixRoom.canSendMessage(type: MessageEventType): Result<Boolean> = canUserSendMessage(sessionId, type)
|
||||
|
||||
/**
|
||||
* Shortcut for calling [MatrixRoom.canUserRedact] with our own user.
|
||||
* Shortcut for calling [MatrixRoom.canUserRedactOwn] with our own user.
|
||||
*/
|
||||
suspend fun MatrixRoom.canRedact(): Result<Boolean> = canUserRedact(sessionId)
|
||||
suspend fun MatrixRoom.canRedactOwn(): Result<Boolean> = canUserRedactOwn(sessionId)
|
||||
|
||||
/**
|
||||
* Shortcut for calling [MatrixRoom.canRedactOther] with our own user.
|
||||
*/
|
||||
suspend fun MatrixRoom.canRedactOther(): Result<Boolean> = canUserRedactOther(sessionId)
|
||||
|
|
|
|||
|
|
@ -36,13 +36,17 @@ sealed interface RoomSummary {
|
|||
data class RoomSummaryDetails(
|
||||
val roomId: RoomId,
|
||||
val name: String,
|
||||
val canonicalAlias: String? = null,
|
||||
val canonicalAlias: String?,
|
||||
val isDirect: Boolean,
|
||||
val avatarURLString: String?,
|
||||
val avatarUrl: String?,
|
||||
val lastMessage: RoomMessage?,
|
||||
val lastMessageTimestamp: Long?,
|
||||
val unreadNotificationCount: Int,
|
||||
val inviter: RoomMember? = null,
|
||||
val notificationMode: RoomNotificationMode? = null,
|
||||
val hasOngoingCall: Boolean = false,
|
||||
)
|
||||
val numUnreadMessages: Int,
|
||||
val numUnreadMentions: Int,
|
||||
val numUnreadNotifications: Int,
|
||||
val inviter: RoomMember?,
|
||||
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||
val hasRoomCall: Boolean,
|
||||
val isDm: Boolean,
|
||||
) {
|
||||
val lastMessageTimestamp = lastMessage?.originServerTs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,11 @@ data class ProfileChangeContent(
|
|||
data class StateContent(
|
||||
val stateKey: String,
|
||||
val content: OtherState
|
||||
) : EventContent
|
||||
) : EventContent {
|
||||
fun isVisibleInTimeline(): Boolean {
|
||||
return content.isVisibleInTimeline()
|
||||
}
|
||||
}
|
||||
|
||||
data class FailedToParseMessageLikeContent(
|
||||
val eventType: String,
|
||||
|
|
|
|||
|
|
@ -41,4 +41,30 @@ sealed interface OtherState {
|
|||
data object SpaceChild : OtherState
|
||||
data object SpaceParent : OtherState
|
||||
data class Custom(val eventType: String) : OtherState
|
||||
|
||||
fun isVisibleInTimeline() = when (this) {
|
||||
// Visible
|
||||
is RoomAvatar,
|
||||
is RoomName,
|
||||
is RoomTopic,
|
||||
is RoomThirdPartyInvite,
|
||||
is RoomCreate,
|
||||
is RoomEncryption,
|
||||
is Custom -> true
|
||||
// Hidden
|
||||
is RoomAliases,
|
||||
is RoomCanonicalAlias,
|
||||
is RoomGuestAccess,
|
||||
is RoomHistoryVisibility,
|
||||
is RoomJoinRules,
|
||||
is RoomPinnedEvents,
|
||||
is RoomPowerLevels,
|
||||
is RoomServerAcl,
|
||||
is RoomTombstone,
|
||||
is SpaceChild,
|
||||
is SpaceParent,
|
||||
is PolicyRuleRoom,
|
||||
is PolicyRuleServer,
|
||||
is PolicyRuleUser -> false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.matrix.api.timeline.item.event
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
@Immutable
|
||||
sealed interface ProfileTimelineDetails {
|
||||
|
|
@ -34,3 +35,20 @@ sealed interface ProfileTimelineDetails {
|
|||
val message: String
|
||||
) : ProfileTimelineDetails
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a disambiguated display name for the user.
|
||||
* If the display name is null, or profile is not Ready, the user ID is returned.
|
||||
* If the display name is ambiguous, the user ID is appended in parentheses.
|
||||
* Otherwise, the display name is returned.
|
||||
*/
|
||||
fun ProfileTimelineDetails.getDisambiguatedDisplayName(userId: UserId): String {
|
||||
return when (this) {
|
||||
is ProfileTimelineDetails.Ready -> when {
|
||||
displayName == null -> userId.value
|
||||
displayNameAmbiguous -> "$displayName ($userId)"
|
||||
else -> displayName
|
||||
}
|
||||
else -> userId.value
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ data class TracingFilterConfiguration(
|
|||
Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.TRACE,
|
||||
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.TRACE,
|
||||
Target.MATRIX_SDK_UI_TIMELINE to LogLevel.TRACE,
|
||||
Target.MATRIX_SDK_BASE_READ_RECEIPTS to LogLevel.TRACE,
|
||||
)
|
||||
|
||||
fun getLogLevel(target: Target): LogLevel {
|
||||
|
|
@ -37,7 +38,7 @@ data class TracingFilterConfiguration(
|
|||
|
||||
val filter: String
|
||||
get() {
|
||||
val fullMap = Target.values().associateWith {
|
||||
val fullMap = Target.entries.associateWith {
|
||||
overrides[it] ?: targetsToLogLevel[it] ?: defaultLogLevel
|
||||
}
|
||||
return fullMap.map {
|
||||
|
|
@ -64,6 +65,7 @@ enum class Target(open val filter: String) {
|
|||
MATRIX_SDK_SLIDING_SYNC("matrix_sdk::sliding_sync"),
|
||||
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"),
|
||||
}
|
||||
|
||||
enum class LogLevel(open val filter: String) {
|
||||
|
|
@ -80,6 +82,12 @@ object TracingFilterConfigurations {
|
|||
Target.ELEMENT to LogLevel.DEBUG
|
||||
),
|
||||
)
|
||||
val nightly = TracingFilterConfiguration(
|
||||
overrides = mapOf(
|
||||
Target.ELEMENT to LogLevel.TRACE,
|
||||
Target.MATRIX_SDK_BASE_READ_RECEIPTS to LogLevel.TRACE,
|
||||
),
|
||||
)
|
||||
val debug = TracingFilterConfiguration(
|
||||
overrides = mapOf(
|
||||
Target.ELEMENT to LogLevel.TRACE
|
||||
|
|
@ -89,7 +97,7 @@ object TracingFilterConfigurations {
|
|||
/**
|
||||
* Use this method to create a custom configuration where all targets will have the same log level.
|
||||
*/
|
||||
fun custom(logLevel: LogLevel) = TracingFilterConfiguration(overrides = Target.values().associateWith { logLevel })
|
||||
fun custom(logLevel: LogLevel) = TracingFilterConfiguration(overrides = Target.entries.associateWith { logLevel })
|
||||
|
||||
/**
|
||||
* Use this method to override the log level of specific targets.
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ package io.element.android.libraries.matrix.api.user
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
/**
|
||||
* Get the current user, as [MatrixUser], using [MatrixClient.loadUserAvatarURLString]
|
||||
* Get the current user, as [MatrixUser], using [MatrixClient.loadUserAvatarUrl]
|
||||
* and [MatrixClient.loadUserDisplayName].
|
||||
*/
|
||||
suspend fun MatrixClient.getCurrentUser(): MatrixUser {
|
||||
val userAvatarUrl = loadUserAvatarURLString().getOrNull()
|
||||
val userAvatarUrl = loadUserAvatarUrl().getOrNull()
|
||||
val userDisplayName = loadUserDisplayName().getOrNull()
|
||||
return MatrixUser(
|
||||
userId = sessionId,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import java.util.UUID
|
|||
interface CallWidgetSettingsProvider {
|
||||
fun provide(
|
||||
baseUrl: String,
|
||||
widgetId: String = UUID.randomUUID().toString()
|
||||
widgetId: String = UUID.randomUUID().toString(),
|
||||
encrypted: Boolean,
|
||||
): MatrixWidgetSettings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.api.notification
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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_USER_ID
|
||||
import org.junit.Test
|
||||
|
||||
class NotificationDataTest {
|
||||
@Test
|
||||
fun `getSenderName should return user id if there is no sender name`() {
|
||||
val sut = aNotificationData(
|
||||
senderDisplayName = null,
|
||||
senderIsNameAmbiguous = false,
|
||||
)
|
||||
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("@alice:server.org")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSenderName should return sender name if defined`() {
|
||||
val sut = aNotificationData(
|
||||
senderDisplayName = "Alice",
|
||||
senderIsNameAmbiguous = false,
|
||||
)
|
||||
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("Alice")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSenderName should return sender name and user id in case of ambiguous display name`() {
|
||||
val sut = aNotificationData(
|
||||
senderDisplayName = "Alice",
|
||||
senderIsNameAmbiguous = true,
|
||||
)
|
||||
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("Alice (@alice:server.org)")
|
||||
}
|
||||
|
||||
private fun aNotificationData(
|
||||
senderDisplayName: String?,
|
||||
senderIsNameAmbiguous: Boolean,
|
||||
): NotificationData {
|
||||
return NotificationData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
senderAvatarUrl = null,
|
||||
senderDisplayName = senderDisplayName,
|
||||
senderIsNameAmbiguous = senderIsNameAmbiguous,
|
||||
roomAvatarUrl = null,
|
||||
roomDisplayName = null,
|
||||
isDirect = false,
|
||||
isEncrypted = false,
|
||||
isNoisy = false,
|
||||
timestamp = 0L,
|
||||
content = NotificationContent.MessageLike.RoomEncrypted,
|
||||
hasMention = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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.timeline.item.event
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import org.junit.Test
|
||||
|
||||
private const val A_USER_ID = "@foo:example.org"
|
||||
private val aUserId = UserId(A_USER_ID)
|
||||
|
||||
class ProfileTimelineDetailsTest {
|
||||
@Test
|
||||
fun `getDisambiguatedDisplayName of Unavailable should be equal to userId`() {
|
||||
assertThat(ProfileTimelineDetails.Unavailable.getDisambiguatedDisplayName(aUserId)).isEqualTo(A_USER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDisambiguatedDisplayName of Error should be equal to userId`() {
|
||||
assertThat(ProfileTimelineDetails.Error("An error").getDisambiguatedDisplayName(aUserId)).isEqualTo(A_USER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDisambiguatedDisplayName of Pending should be equal to userId`() {
|
||||
assertThat(ProfileTimelineDetails.Pending.getDisambiguatedDisplayName(aUserId)).isEqualTo(A_USER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDisambiguatedDisplayName of Ready without display name should be equal to userId`() {
|
||||
assertThat(
|
||||
ProfileTimelineDetails.Ready(
|
||||
displayName = null,
|
||||
displayNameAmbiguous = false,
|
||||
avatarUrl = null,
|
||||
).getDisambiguatedDisplayName(aUserId)
|
||||
).isEqualTo(A_USER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDisambiguatedDisplayName of Ready with display name should be equal to display name`() {
|
||||
assertThat(
|
||||
ProfileTimelineDetails.Ready(
|
||||
displayName = "Alice",
|
||||
displayNameAmbiguous = false,
|
||||
avatarUrl = null,
|
||||
).getDisambiguatedDisplayName(aUserId)
|
||||
).isEqualTo("Alice")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDisambiguatedDisplayName of Ready with display name and ambiguous should be equal to display name with user id`() {
|
||||
assertThat(
|
||||
ProfileTimelineDetails.Ready(
|
||||
displayName = "Alice",
|
||||
displayNameAmbiguous = true,
|
||||
avatarUrl = null,
|
||||
).getDisambiguatedDisplayName(aUserId)
|
||||
).isEqualTo("Alice ($A_USER_ID)")
|
||||
}
|
||||
}
|
||||
|
|
@ -53,4 +53,5 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.turbine)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,11 +76,14 @@ 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.FilterStateEventType
|
||||
import org.matrix.rustcomponents.sdk.FilterTimelineEventType
|
||||
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||
import org.matrix.rustcomponents.sdk.PowerLevels
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
|
@ -102,9 +105,11 @@ class RustMatrixClient(
|
|||
private val clock: SystemClock,
|
||||
) : MatrixClient {
|
||||
override val sessionId: UserId = UserId(client.userId())
|
||||
override val deviceId: String = client.deviceId()
|
||||
override val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId")
|
||||
|
||||
private val innerRoomListService = syncService.roomListService()
|
||||
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
||||
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId")
|
||||
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
|
||||
private val verificationService = RustSessionVerificationService(rustSyncService, sessionCoroutineScope)
|
||||
private val pushersService = RustPushersService(
|
||||
|
|
@ -139,15 +144,25 @@ class RustMatrixClient(
|
|||
// TODO handle isSoftLogout parameter.
|
||||
appCoroutineScope.launch {
|
||||
val existingData = sessionStore.getSession(client.userId())
|
||||
val anonymizedToken = existingData?.accessToken?.takeLast(4)
|
||||
Timber.d("Removing session data with token: '...$anonymizedToken'.")
|
||||
if (existingData != null) {
|
||||
// Set isTokenValid to false
|
||||
val newData = client.session().toSessionData(
|
||||
isTokenValid = false,
|
||||
loginType = existingData.loginType,
|
||||
passphrase = existingData.passphrase,
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
Timber.d("Removed session data with token: '...$anonymizedToken'.")
|
||||
} else {
|
||||
Timber.d("No session data found.")
|
||||
}
|
||||
doLogout(doRequest = false, removeSession = false, ignoreSdkError = false)
|
||||
}.invokeOnCompletion {
|
||||
if (it != null) {
|
||||
Timber.e(it, "Failed to remove session data.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.v("didReceiveAuthError -> already cleaning up")
|
||||
|
|
@ -158,11 +173,19 @@ class RustMatrixClient(
|
|||
Timber.w("didRefreshTokens()")
|
||||
appCoroutineScope.launch {
|
||||
val existingData = sessionStore.getSession(client.userId()) ?: return@launch
|
||||
val anonymizedToken = client.session().accessToken.takeLast(4)
|
||||
Timber.d("Saving new session data with token: '...$anonymizedToken'. Was token valid: ${existingData.isTokenValid}")
|
||||
val newData = client.session().toSessionData(
|
||||
isTokenValid = existingData.isTokenValid,
|
||||
isTokenValid = true,
|
||||
loginType = existingData.loginType,
|
||||
passphrase = existingData.passphrase,
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
Timber.d("Saved new session data with token: '...$anonymizedToken'.")
|
||||
}.invokeOnCompletion {
|
||||
if (it != null) {
|
||||
Timber.e(it, "Failed to save new session data.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -178,6 +201,25 @@ class RustMatrixClient(
|
|||
),
|
||||
)
|
||||
|
||||
private val eventFilters = TimelineEventTypeFilter.exclude(
|
||||
listOf(
|
||||
FilterStateEventType.ROOM_ALIASES,
|
||||
FilterStateEventType.ROOM_CANONICAL_ALIAS,
|
||||
FilterStateEventType.ROOM_GUEST_ACCESS,
|
||||
FilterStateEventType.ROOM_HISTORY_VISIBILITY,
|
||||
FilterStateEventType.ROOM_JOIN_RULES,
|
||||
FilterStateEventType.ROOM_PINNED_EVENTS,
|
||||
FilterStateEventType.ROOM_POWER_LEVELS,
|
||||
FilterStateEventType.ROOM_SERVER_ACL,
|
||||
FilterStateEventType.ROOM_TOMBSTONE,
|
||||
FilterStateEventType.SPACE_CHILD,
|
||||
FilterStateEventType.SPACE_PARENT,
|
||||
FilterStateEventType.POLICY_RULE_ROOM,
|
||||
FilterStateEventType.POLICY_RULE_SERVER,
|
||||
FilterStateEventType.POLICY_RULE_USER,
|
||||
).map(FilterTimelineEventType::State)
|
||||
)
|
||||
|
||||
override val roomListService: RoomListService
|
||||
get() = rustRoomListService
|
||||
|
||||
|
|
@ -226,10 +268,14 @@ class RustMatrixClient(
|
|||
}
|
||||
}
|
||||
|
||||
private fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
|
||||
private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
|
||||
val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
|
||||
// Keep using fullRoomBlocking for now as it's faster.
|
||||
val fullRoom = cachedRoomListItem?.fullRoomBlocking()
|
||||
val fullRoom = cachedRoomListItem?.let { roomListItem ->
|
||||
if (!roomListItem.isTimelineInitialized()) {
|
||||
roomListItem.initTimeline(eventFilters)
|
||||
}
|
||||
roomListItem.fullRoom()
|
||||
}
|
||||
return if (cachedRoomListItem == null || fullRoom == null) {
|
||||
Timber.d("No room cached for $roomId")
|
||||
null
|
||||
|
|
@ -411,7 +457,7 @@ class RustMatrixClient(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun loadUserAvatarURLString(): Result<String?> = withContext(sessionDispatcher) {
|
||||
override suspend fun loadUserAvatarUrl(): Result<String?> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
client.avatarUrl()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class RustMatrixClientFactory @Inject constructor(
|
|||
.basePath(baseDirectory.absolutePath)
|
||||
.homeserverUrl(sessionData.homeserverUrl)
|
||||
.username(sessionData.userId)
|
||||
.passphrase(sessionData.passphrase)
|
||||
.userAgent(userAgentProvider.provide())
|
||||
// FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
|
||||
.serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ package io.element.android.libraries.matrix.impl.auth
|
|||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -28,6 +30,7 @@ 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.keys.PassphraseGenerator
|
||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
|
|
@ -39,6 +42,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
|
||||
|
|
@ -51,10 +55,15 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
private val sessionStore: SessionStore,
|
||||
userAgentProvider: UserAgentProvider,
|
||||
private val rustMatrixClientFactory: RustMatrixClientFactory,
|
||||
private val passphraseGenerator: PassphraseGenerator,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : MatrixAuthenticationService {
|
||||
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
|
||||
// stored in the SessionData.
|
||||
private val pendingPassphrase = getDatabasePassphrase()
|
||||
private val authService: RustAuthenticationService = RustAuthenticationService(
|
||||
basePath = baseDirectory.absolutePath,
|
||||
passphrase = null,
|
||||
passphrase = pendingPassphrase,
|
||||
userAgent = userAgentProvider.provide(),
|
||||
oidcConfiguration = oidcConfiguration,
|
||||
customSlidingSyncProxy = null,
|
||||
|
|
@ -76,6 +85,12 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
val sessionData = sessionStore.getSession(sessionId.value)
|
||||
if (sessionData != null) {
|
||||
if (sessionData.isTokenValid) {
|
||||
// Use the sessionData.passphrase, which can be null for a previously created session
|
||||
if (sessionData.passphrase == null) {
|
||||
Timber.w("Restoring a session without a passphrase")
|
||||
} else {
|
||||
Timber.w("Restoring a session with a passphrase")
|
||||
}
|
||||
rustMatrixClientFactory.create(sessionData)
|
||||
} else {
|
||||
error("Token is not valid")
|
||||
|
|
@ -88,6 +103,21 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getDatabasePassphrase(): String? {
|
||||
// TODO Remove this if block at some point
|
||||
// Return a passphrase only for debug and nightly build for now
|
||||
if (buildMeta.buildType == BuildType.RELEASE) {
|
||||
Timber.w("New sessions will not be encrypted with a passphrase (release build)")
|
||||
return null
|
||||
}
|
||||
|
||||
val passphrase = passphraseGenerator.generatePassphrase()
|
||||
if (passphrase != null) {
|
||||
Timber.w("New sessions will be encrypted with a passphrase")
|
||||
}
|
||||
return passphrase
|
||||
}
|
||||
|
||||
override fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?> = currentHomeserver
|
||||
|
||||
override suspend fun setHomeserver(homeserver: String): Result<Unit> =
|
||||
|
|
@ -111,6 +141,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
it.session().toSessionData(
|
||||
isTokenValid = true,
|
||||
loginType = LoginType.PASSWORD,
|
||||
passphrase = pendingPassphrase,
|
||||
)
|
||||
}
|
||||
sessionStore.storeData(sessionData)
|
||||
|
|
@ -158,6 +189,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
it.session().toSessionData(
|
||||
isTokenValid = true,
|
||||
loginType = LoginType.OIDC,
|
||||
passphrase = pendingPassphrase
|
||||
)
|
||||
}
|
||||
pendingOidcAuthenticationData?.close()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesTo
|
|||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
|
|
@ -27,6 +28,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
|
|||
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.verification.SessionVerificationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Module
|
||||
@ContributesTo(SessionScope::class)
|
||||
|
|
@ -60,4 +62,10 @@ object SessionMatrixModule {
|
|||
fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader {
|
||||
return matrixClient.mediaLoader
|
||||
}
|
||||
|
||||
@SessionCoroutineScope
|
||||
@Provides
|
||||
fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope {
|
||||
return matrixClient.sessionCoroutineScope
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.impl.keys
|
||||
|
||||
import android.util.Base64
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SECRET_SIZE = 256
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPassphraseGenerator @Inject constructor() : PassphraseGenerator {
|
||||
override fun generatePassphrase(): String? {
|
||||
val key = ByteArray(size = SECRET_SIZE)
|
||||
SecureRandom().nextBytes(key)
|
||||
return Base64.encodeToString(key, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.impl.keys
|
||||
|
||||
interface PassphraseGenerator {
|
||||
/**
|
||||
* Generate a passphrase to encrypt the databases of a session.
|
||||
* Return null to not encrypt the databases.
|
||||
*/
|
||||
fun generatePassphrase(): String?
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import java.util.Date
|
|||
internal fun Session.toSessionData(
|
||||
isTokenValid: Boolean,
|
||||
loginType: LoginType,
|
||||
passphrase: String?,
|
||||
) = SessionData(
|
||||
userId = userId,
|
||||
deviceId = deviceId,
|
||||
|
|
@ -35,4 +36,5 @@ internal fun Session.toSessionData(
|
|||
loginTimestamp = Date(),
|
||||
isTokenValid = isTokenValid,
|
||||
loginType = loginType,
|
||||
passphrase = passphrase,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class NotificationMapper(
|
|||
roomId = roomId,
|
||||
senderAvatarUrl = item.senderInfo.avatarUrl,
|
||||
senderDisplayName = item.senderInfo.displayName,
|
||||
senderIsNameAmbiguous = item.senderInfo.isNameAmbiguous,
|
||||
roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { item.roomInfo.isDirect },
|
||||
roomDisplayName = item.roomInfo.displayName,
|
||||
isDirect = item.roomInfo.isDirect,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.libraries.matrix.impl.notification
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.impl.room.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
|
||||
import org.matrix.rustcomponents.sdk.MessageLikeEventContent
|
||||
import org.matrix.rustcomponents.sdk.StateEventContent
|
||||
|
|
|
|||
|
|
@ -143,6 +143,6 @@ class RustNotificationSettingsService(
|
|||
|
||||
override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean> =
|
||||
runCatching {
|
||||
notificationSettings.canHomeserverPushEncryptedEventToDevice()
|
||||
notificationSettings.canPushEncryptedEventToDevice()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.room
|
|||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package io.element.android.libraries.matrix.impl.room
|
|||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.core.coroutine.parallelMap
|
||||
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
|
||||
|
|
@ -39,7 +38,6 @@ import io.element.android.libraries.matrix.api.room.Mention
|
|||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||
|
|
@ -51,20 +49,16 @@ import io.element.android.libraries.matrix.impl.media.toMSC3246range
|
|||
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
|
||||
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.AsyncMatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
|
||||
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -75,7 +69,6 @@ import org.matrix.rustcomponents.sdk.EventTimelineItem
|
|||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomMember
|
||||
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||
import org.matrix.rustcomponents.sdk.WidgetCapabilities
|
||||
|
|
@ -125,8 +118,8 @@ class RustMatrixRoom(
|
|||
private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8)
|
||||
|
||||
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
|
||||
private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
|
||||
private val _syncUpdateFlow = MutableStateFlow(0L)
|
||||
private val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher)
|
||||
|
||||
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
|
||||
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
|
||||
|
|
@ -135,7 +128,7 @@ class RustMatrixRoom(
|
|||
_syncUpdateFlow.value = systemClock.epochMillis()
|
||||
}
|
||||
|
||||
override val membersStateFlow: StateFlow<MatrixRoomMembersState> = _membersStateFlow.asStateFlow()
|
||||
override val membersStateFlow: StateFlow<MatrixRoomMembersState> = roomMemberListFetcher.membersFlow
|
||||
|
||||
override val syncUpdateFlow: StateFlow<Long> = _syncUpdateFlow.asStateFlow()
|
||||
|
||||
|
|
@ -192,35 +185,7 @@ class RustMatrixRoom(
|
|||
override val activeMemberCount: Long
|
||||
get() = innerRoom.activeMembersCount().toLong()
|
||||
|
||||
override suspend fun updateMembers(): Result<Unit> = withContext(roomMembersDispatcher) {
|
||||
val currentState = _membersStateFlow.value
|
||||
val currentMembers = currentState.roomMembers()?.toImmutableList()
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers)
|
||||
var rustMembers: List<RoomMember>? = null
|
||||
try {
|
||||
rustMembers = innerRoom.members().use { membersIterator ->
|
||||
buildList {
|
||||
while (true) {
|
||||
// Loading the whole membersIterator as a stop-gap measure.
|
||||
// We should probably implement some sort of paging in the future.
|
||||
ensureActive()
|
||||
addAll(membersIterator.nextChunk(1000u) ?: break)
|
||||
}
|
||||
}
|
||||
}
|
||||
val mappedMembers = rustMembers.parallelMap(RoomMemberMapper::map)
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Ready(mappedMembers.toImmutableList())
|
||||
Result.success(Unit)
|
||||
} catch (exception: CancellationException) {
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = exception)
|
||||
throw exception
|
||||
} catch (exception: Exception) {
|
||||
_membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = exception)
|
||||
Result.failure(exception)
|
||||
} finally {
|
||||
rustMembers?.destroyAll()
|
||||
}
|
||||
}
|
||||
override suspend fun updateMembers() = roomMemberListFetcher.fetchRoomMembers()
|
||||
|
||||
override suspend fun userDisplayName(userId: UserId): Result<String?> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
|
|
@ -335,9 +300,15 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun canUserRedact(userId: UserId): Result<Boolean> {
|
||||
override suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> {
|
||||
return runCatching {
|
||||
innerRoom.canUserRedact(userId.value)
|
||||
innerRoom.canUserRedactOwn(userId.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun canUserRedactOther(userId: UserId): Result<Boolean> {
|
||||
return runCatching {
|
||||
innerRoom.canUserRedactOther(userId.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -548,6 +519,10 @@ class RustMatrixRoom(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun typingNotice(isTyping: Boolean) = runCatching {
|
||||
innerRoom.typingNotice(isTyping)
|
||||
}
|
||||
|
||||
override suspend fun generateWidgetWebViewUrl(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
clientId: String,
|
||||
|
|
@ -569,14 +544,6 @@ class RustMatrixRoom(
|
|||
)
|
||||
}
|
||||
|
||||
override fun pollHistory() = AsyncMatrixTimeline(
|
||||
coroutineScope = roomCoroutineScope,
|
||||
dispatcher = roomDispatcher
|
||||
) {
|
||||
val innerTimeline = innerRoom.pollHistory()
|
||||
createMatrixTimeline(innerTimeline)
|
||||
}
|
||||
|
||||
private fun sendAttachment(files: List<File>, handle: () -> SendAttachmentJoinHandle): Result<MediaUploadHandler> {
|
||||
return runCatching {
|
||||
MediaUploadHandlerImpl(files, handle())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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.impl.room.member
|
||||
|
||||
import io.element.android.libraries.core.coroutine.parallelMap
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.impl.util.destroyAll
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.RoomInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomMembersIterator
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
/**
|
||||
* This class fetches the room members for a given room in a 'paginated' way, and taking into account previous cached values.
|
||||
*/
|
||||
internal class RoomMemberListFetcher(
|
||||
private val room: RoomInterface,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val pageSize: Int = 1000,
|
||||
) {
|
||||
private val updatedRoomMemberMutex = Mutex()
|
||||
private val roomId = room.id()
|
||||
|
||||
private val _membersFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
|
||||
val membersFlow: StateFlow<MatrixRoomMembersState> = _membersFlow
|
||||
|
||||
/**
|
||||
* Fetches the room members for the given room.
|
||||
* It will emit the cached members first, and then the updated members in batches of [pageSize] items, through [membersFlow].
|
||||
* @param withCache Whether to load the cached members first. Defaults to true.
|
||||
*/
|
||||
suspend fun fetchRoomMembers(withCache: Boolean = true) {
|
||||
if (updatedRoomMemberMutex.isLocked) {
|
||||
Timber.i("Room members are already being updated for room $roomId")
|
||||
return
|
||||
}
|
||||
updatedRoomMemberMutex.withLock {
|
||||
withContext(dispatcher) {
|
||||
// Load cached members as fallback and to get faster results
|
||||
if (withCache) {
|
||||
if (_membersFlow.value !is MatrixRoomMembersState.Ready) {
|
||||
fetchCachedRoomMembers()
|
||||
} else {
|
||||
Timber.i("No need to load cached members found for room $roomId")
|
||||
}
|
||||
}
|
||||
|
||||
val prevRoomMembers = (_membersFlow.value as? MatrixRoomMembersState.Ready)?.roomMembers?.toImmutableList()
|
||||
_membersFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = prevRoomMembers)
|
||||
|
||||
try {
|
||||
// Start loading new members
|
||||
parseAndEmitMembers(room.members())
|
||||
} catch (exception: CancellationException) {
|
||||
Timber.d("Cancelled loading updated members for room $roomId")
|
||||
throw exception
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception, "Failed to load updated members for room $roomId")
|
||||
_membersFlow.value = MatrixRoomMembersState.Error(exception, prevRoomMembers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun fetchCachedRoomMembers() = withContext(dispatcher) {
|
||||
Timber.i("Loading cached members for room $roomId")
|
||||
try {
|
||||
val iterator = room.membersNoSync()
|
||||
parseAndEmitMembers(iterator)
|
||||
} catch (exception: CancellationException) {
|
||||
Timber.d("Cancelled loading cached members for room $roomId")
|
||||
throw exception
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception, "Failed to load cached members for room $roomId")
|
||||
_membersFlow.value = MatrixRoomMembersState.Error(exception, _membersFlow.value.roomMembers()?.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseAndEmitMembers(roomMembersIterator: RoomMembersIterator) {
|
||||
roomMembersIterator.use { iterator ->
|
||||
val results = buildList {
|
||||
while (true) {
|
||||
// Loading the whole membersIterator as a stop-gap measure.
|
||||
// We should probably implement some sort of paging in the future.
|
||||
coroutineContext.ensureActive()
|
||||
val chunk = iterator.nextChunk(pageSize.toUInt())
|
||||
val members = try {
|
||||
// Load next chunk. If null (no more items), exit the loop
|
||||
chunk?.parallelMap(RoomMemberMapper::map) ?: break
|
||||
} finally {
|
||||
// Make sure we clear all member references
|
||||
chunk?.destroyAll()
|
||||
}
|
||||
addAll(members)
|
||||
Timber.i("Emitting first $size members for room $roomId")
|
||||
_membersFlow.value = MatrixRoomMembersState.Ready(toImmutableList())
|
||||
}
|
||||
}
|
||||
if (results.isEmpty()) {
|
||||
_membersFlow.value = MatrixRoomMembersState.Ready(results.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* 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.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room
|
||||
package io.element.android.libraries.matrix.impl.room.member
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
|
@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.impl.roomlist
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
|
||||
import io.element.android.libraries.matrix.impl.room.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
|
|
@ -34,13 +34,15 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
|
|||
name = roomInfo.name ?: roomInfo.id,
|
||||
canonicalAlias = roomInfo.canonicalAlias,
|
||||
isDirect = roomInfo.isDirect,
|
||||
avatarURLString = roomInfo.avatarUrl,
|
||||
unreadNotificationCount = roomInfo.notificationCount.toInt(),
|
||||
avatarUrl = roomInfo.avatarUrl,
|
||||
numUnreadMentions = roomInfo.numUnreadMentions.toInt(),
|
||||
numUnreadMessages = roomInfo.numUnreadMessages.toInt(),
|
||||
numUnreadNotifications = roomInfo.numUnreadNotifications.toInt(),
|
||||
lastMessage = latestRoomMessage,
|
||||
lastMessageTimestamp = latestRoomMessage?.originServerTs,
|
||||
inviter = roomInfo.inviter?.let(RoomMemberMapper::map),
|
||||
notificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),
|
||||
hasOngoingCall = roomInfo.hasRoomCall,
|
||||
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),
|
||||
hasRoomCall = roomInfo.hasRoomCall,
|
||||
isDm = roomInfo.isDirect && roomInfo.activeMembersCount.toLong() == 2L,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import timber.log.Timber
|
|||
/**
|
||||
* This class is a wrapper around a [MatrixTimeline] that will be created asynchronously.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class AsyncMatrixTimeline(
|
||||
coroutineScope: CoroutineScope,
|
||||
dispatcher: CoroutineDispatcher,
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import org.matrix.rustcomponents.sdk.BackPaginationStatus
|
||||
import org.matrix.rustcomponents.sdk.BackPaginationStatusListener
|
||||
import org.matrix.rustcomponents.sdk.Timeline
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_ui.BackPaginationStatus
|
||||
|
||||
internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List<TimelineItem>) -> Unit): Flow<List<TimelineDiff>> =
|
||||
callbackFlow {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage
|
|||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.DmBeginningTimelineProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterHiddenStateEventsProcessor
|
||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
|
@ -44,13 +46,13 @@ import kotlinx.coroutines.flow.mapLatest
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.BackPaginationStatus
|
||||
import org.matrix.rustcomponents.sdk.EventItemOrigin
|
||||
import org.matrix.rustcomponents.sdk.PaginationOptions
|
||||
import org.matrix.rustcomponents.sdk.Timeline
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk_ui.BackPaginationStatus
|
||||
import uniffi.matrix_sdk_ui.EventItemOrigin
|
||||
import java.util.Date
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
|
@ -82,6 +84,10 @@ class RustMatrixTimeline(
|
|||
dispatcher = dispatcher,
|
||||
)
|
||||
|
||||
private val filterHiddenStateEventsProcessor = FilterHiddenStateEventsProcessor()
|
||||
|
||||
private val dmBeginningTimelineProcessor = DmBeginningTimelineProcessor()
|
||||
|
||||
private val timelineItemFactory = MatrixTimelineItemMapper(
|
||||
fetchDetailsForEvent = this::fetchDetailsForEvent,
|
||||
roomCoroutineScope = roomCoroutineScope,
|
||||
|
|
@ -101,9 +107,16 @@ class RustMatrixTimeline(
|
|||
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState.asStateFlow()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems.mapLatest { items ->
|
||||
encryptedHistoryPostProcessor.process(items)
|
||||
}
|
||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
|
||||
.mapLatest { items -> encryptedHistoryPostProcessor.process(items) }
|
||||
.mapLatest { items -> filterHiddenStateEventsProcessor.process(items) }
|
||||
.mapLatest { items ->
|
||||
dmBeginningTimelineProcessor.process(
|
||||
items = items,
|
||||
isDm = matrixRoom.isDirect && matrixRoom.isOneToOne,
|
||||
isAtStartOfTimeline = paginationState.value.beginningOfRoomReached
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.timeline
|
||||
|
||||
import org.matrix.rustcomponents.sdk.EventItemOrigin
|
||||
import org.matrix.rustcomponents.sdk.TimelineChange
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||
import uniffi.matrix_sdk_ui.EventItemOrigin
|
||||
|
||||
/**
|
||||
* Tries to get an event origin from the TimelineDiff.
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.matrix.rustcomponents.sdk.Reaction
|
||||
import org.matrix.rustcomponents.sdk.EventItemOrigin as RustEventItemOrigin
|
||||
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
|
||||
import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails
|
||||
import org.matrix.rustcomponents.sdk.Receipt as RustReceipt
|
||||
import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin
|
||||
|
||||
class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper()) {
|
||||
fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.impl.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
|
||||
/**
|
||||
* This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs.
|
||||
*/
|
||||
class DmBeginningTimelineProcessor {
|
||||
fun process(
|
||||
items: List<MatrixTimelineItem>,
|
||||
isDm: Boolean,
|
||||
isAtStartOfTimeline: Boolean
|
||||
): List<MatrixTimelineItem> {
|
||||
if (!isDm || !isAtStartOfTimeline) return items
|
||||
|
||||
// Find room creation event. This is usually index 0
|
||||
val roomCreationEventIndex = items.indexOfFirst {
|
||||
val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? StateContent
|
||||
stateEventContent?.content is OtherState.RoomCreate
|
||||
}
|
||||
|
||||
// Find self-join event for room creator. This is usually index 1
|
||||
val roomCreatorUserId = (items.getOrNull(roomCreationEventIndex) as? MatrixTimelineItem.Event)?.event?.sender
|
||||
val selfUserJoinedEventIndex = roomCreatorUserId?.let { creatorUserId ->
|
||||
items.indexOfFirst {
|
||||
val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? RoomMembershipContent
|
||||
stateEventContent?.change == MembershipChange.JOINED && stateEventContent.userId == creatorUserId
|
||||
}
|
||||
} ?: -1
|
||||
|
||||
// Remove items at the indices we found
|
||||
val newItems = items.toMutableList()
|
||||
if (selfUserJoinedEventIndex in newItems.indices) {
|
||||
newItems.removeAt(selfUserJoinedEventIndex)
|
||||
}
|
||||
if (roomCreationEventIndex in newItems.indices) {
|
||||
newItems.removeAt(roomCreationEventIndex)
|
||||
}
|
||||
return newItems
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*
|
||||
* 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.timeline.postprocessor
|
||||
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
|
||||
/**
|
||||
* This class is used to filter out 'hidden' state events from the timeline.
|
||||
*/
|
||||
class FilterHiddenStateEventsProcessor {
|
||||
fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
||||
return items.filter { item ->
|
||||
when (item) {
|
||||
is MatrixTimelineItem.Event -> {
|
||||
when (val content = item.event.content) {
|
||||
// If it's a state event, make sure it's visible
|
||||
is StateContent -> content.isVisibleInTimeline()
|
||||
// We can display any other event
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
is MatrixTimelineItem.Virtual -> true
|
||||
is MatrixTimelineItem.Other -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ import javax.inject.Inject
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettingsProvider {
|
||||
override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings {
|
||||
override fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
|
||||
val options = VirtualElementCallWidgetOptions(
|
||||
elementCallUrl = baseUrl,
|
||||
widgetId = widgetId,
|
||||
|
|
@ -40,7 +40,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettin
|
|||
confineToRoom = true,
|
||||
font = null,
|
||||
analyticsId = null,
|
||||
encryption = EncryptionSystem.PerParticipantKeys,
|
||||
encryption = if (encrypted) EncryptionSystem.PerParticipantKeys else EncryptionSystem.Unencrypted,
|
||||
)
|
||||
val rustWidgetSettings = newVirtualElementCallWidget(options)
|
||||
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
* 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.impl.room.member
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.MembershipState
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomMember
|
||||
import org.matrix.rustcomponents.sdk.RoomMembersIterator
|
||||
|
||||
class RoomMemberListFetcherTest {
|
||||
@Test
|
||||
fun `fetchCachedRoomMembers - emits cached members, if any`() = runTest {
|
||||
val room = FakeRustRoom(getMembersNoSync = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||
fetcher.membersFlow.test {
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||
|
||||
fetcher.fetchCachedRoomMembers()
|
||||
|
||||
val readyItem = awaitItem()
|
||||
assertThat(readyItem).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
||||
assertThat((readyItem as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchCachedRoomMembers - emits empty list, if no members exist`() = runTest {
|
||||
val room = FakeRustRoom(getMembersNoSync = {
|
||||
FakeRoomMembersIterator(emptyList())
|
||||
})
|
||||
|
||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||
fetcher.membersFlow.test {
|
||||
fetcher.fetchCachedRoomMembers()
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||
assertThat(awaitItem().roomMembers()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchCachedRoomMembers - emits Error on error found`() = runTest {
|
||||
val room = FakeRustRoom(getMembersNoSync = {
|
||||
error("Some unexpected issue")
|
||||
})
|
||||
|
||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||
fetcher.membersFlow.test {
|
||||
fetcher.fetchCachedRoomMembers()
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchCachedRoomMembers - emits items using page size`() = runTest {
|
||||
val room = FakeRustRoom(getMembersNoSync = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default, pageSize = 2)
|
||||
fetcher.membersFlow.test {
|
||||
fetcher.fetchCachedRoomMembers()
|
||||
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(2)
|
||||
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
|
||||
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchRoomMembers - with 'withCache' set to false emits only new members, if any`() = runTest {
|
||||
val room = FakeRustRoom(getMembers = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||
fetcher.membersFlow.test {
|
||||
fetcher.fetchRoomMembers(withCache = false)
|
||||
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchRoomMembers - on error it emits an Error item`() = runTest {
|
||||
val room = FakeRustRoom(getMembers = { error("An unexpected error") })
|
||||
|
||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||
fetcher.membersFlow.test {
|
||||
fetcher.fetchRoomMembers(withCache = false)
|
||||
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Error::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchRoomMembers - with 'withCache' returns cached items first, then new ones`() = runTest {
|
||||
val room = FakeRustRoom(
|
||||
getMembersNoSync = {
|
||||
FakeRoomMembersIterator(listOf(FakeRustRoomMember(A_USER_ID_4)))
|
||||
},
|
||||
getMembers = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||
fetcher.membersFlow.test {
|
||||
fetcher.fetchRoomMembers(withCache = true)
|
||||
// Initial
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
|
||||
// Loaded cached
|
||||
awaitItem().let { cached ->
|
||||
assertThat(cached).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
||||
assertThat(cached.roomMembers()).hasSize(1)
|
||||
}
|
||||
// Start loading new
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||
awaitItem().let { ready ->
|
||||
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
||||
assertThat(ready.roomMembers()).hasSize(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchRoomMembers - with 'withCache' skips cache if there is already a ready state`() = runTest {
|
||||
val room = FakeRustRoom(
|
||||
getMembersNoSync = {
|
||||
FakeRoomMembersIterator(listOf(FakeRustRoomMember(A_USER_ID_4)))
|
||||
},
|
||||
getMembers = {
|
||||
FakeRoomMembersIterator(
|
||||
listOf(
|
||||
FakeRustRoomMember(A_USER_ID),
|
||||
FakeRustRoomMember(A_USER_ID_2),
|
||||
FakeRustRoomMember(A_USER_ID_3),
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
|
||||
// Set a ready state
|
||||
fetcher.fetchRoomMembers(withCache = false)
|
||||
|
||||
fetcher.membersFlow.test {
|
||||
// Start loading new members
|
||||
fetcher.fetchRoomMembers(withCache = true)
|
||||
// Previous ready state
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
||||
// New pending state
|
||||
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
|
||||
// New ready state
|
||||
awaitItem().let { ready ->
|
||||
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
|
||||
assertThat(ready.roomMembers()).hasSize(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FakeRustRoom(
|
||||
private val getMembers: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
|
||||
private val getMembersNoSync: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
|
||||
) : Room(NoPointer) {
|
||||
override fun id(): String {
|
||||
return A_ROOM_ID.value
|
||||
}
|
||||
|
||||
override suspend fun members(): RoomMembersIterator {
|
||||
return getMembers()
|
||||
}
|
||||
|
||||
override suspend fun membersNoSync(): RoomMembersIterator {
|
||||
return getMembersNoSync()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
class FakeRoomMembersIterator(
|
||||
private var members: List<RoomMember>? = null
|
||||
) : RoomMembersIterator(NoPointer) {
|
||||
override fun len(): UInt {
|
||||
return members?.size?.toUInt() ?: 0u
|
||||
}
|
||||
|
||||
override fun nextChunk(chunkSize: UInt): List<RoomMember>? {
|
||||
if (members?.isEmpty() == true) {
|
||||
return null
|
||||
}
|
||||
return members?.let {
|
||||
val result = it.take(chunkSize.toInt())
|
||||
members = it.subList(result.size, it.size)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FakeRustRoomMember(
|
||||
private val userId: UserId,
|
||||
private val displayName: String? = null,
|
||||
private val avatarUrl: String? = null,
|
||||
private val membership: MembershipState = MembershipState.JOIN,
|
||||
private val isNameAmbiguous: Boolean = false,
|
||||
private val powerLevel: Long = 0L,
|
||||
) : RoomMember(NoPointer) {
|
||||
override fun userId(): String {
|
||||
return userId.value
|
||||
}
|
||||
|
||||
override fun displayName(): String? {
|
||||
return displayName
|
||||
}
|
||||
|
||||
override fun avatarUrl(): String? {
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
override fun membership(): MembershipState {
|
||||
return membership
|
||||
}
|
||||
|
||||
override fun isNameAmbiguous(): Boolean {
|
||||
return isNameAmbiguous
|
||||
}
|
||||
|
||||
override fun powerLevel(): Long {
|
||||
return powerLevel
|
||||
}
|
||||
|
||||
override fun normalizedPowerLevel(): Long {
|
||||
return powerLevel
|
||||
}
|
||||
|
||||
override fun isIgnored(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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.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.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import org.junit.Test
|
||||
|
||||
class DmBeginningTimelineProcessorTest {
|
||||
@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, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = DmBeginningTimelineProcessor()
|
||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = true)
|
||||
assertThat(processedItems).isEmpty()
|
||||
}
|
||||
|
||||
@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, 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, MembershipChange.JOINED))),
|
||||
)
|
||||
val expected = listOf(
|
||||
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
|
||||
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
|
||||
)
|
||||
val processor = DmBeginningTimelineProcessor()
|
||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = true)
|
||||
assertThat(processedItems).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processor won't remove items 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, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = DmBeginningTimelineProcessor()
|
||||
val processedItems = processor.process(timelineItems, isDm = false, isAtStartOfTimeline = true)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
||||
@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, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = DmBeginningTimelineProcessor()
|
||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
||||
@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, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = DmBeginningTimelineProcessor()
|
||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
|
||||
@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, MembershipChange.JOINED))),
|
||||
)
|
||||
val processor = DmBeginningTimelineProcessor()
|
||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false)
|
||||
assertThat(processedItems).isEqualTo(timelineItems)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.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.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import org.junit.Test
|
||||
|
||||
class FilterHiddenStateEventsProcessorTest {
|
||||
@Test
|
||||
fun test() {
|
||||
val items = listOf(
|
||||
// These are visible because they're not state events
|
||||
MatrixTimelineItem.Other,
|
||||
MatrixTimelineItem.Virtual("virtual", VirtualTimelineItem.ReadMarker),
|
||||
MatrixTimelineItem.Event("event", anEventTimelineItem()),
|
||||
// These are visible state events
|
||||
MatrixTimelineItem.Event("m.room.avatar", anEventTimelineItem(content = StateContent("", OtherState.RoomAvatar("")))),
|
||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event("m.room.encrypted", anEventTimelineItem(content = StateContent("", OtherState.RoomEncryption))),
|
||||
MatrixTimelineItem.Event("m.room.name", anEventTimelineItem(content = StateContent("", OtherState.RoomName("")))),
|
||||
MatrixTimelineItem.Event("m.room.third_party_invite", anEventTimelineItem(content = StateContent("", OtherState.RoomThirdPartyInvite("")))),
|
||||
MatrixTimelineItem.Event("m.room.topic", anEventTimelineItem(content = StateContent("", OtherState.RoomTopic("")))),
|
||||
MatrixTimelineItem.Event("m.room.custom", anEventTimelineItem(content = StateContent("", OtherState.Custom("")))),
|
||||
// These ones are hidden
|
||||
MatrixTimelineItem.Event("m.room.aliases", anEventTimelineItem(content = StateContent("", OtherState.RoomAliases))),
|
||||
MatrixTimelineItem.Event("m.room.canonical_alias", anEventTimelineItem(content = StateContent("", OtherState.RoomCanonicalAlias))),
|
||||
MatrixTimelineItem.Event("m.room.guest_access", anEventTimelineItem(content = StateContent("", OtherState.RoomGuestAccess))),
|
||||
MatrixTimelineItem.Event("m.room.history_visibility", anEventTimelineItem(content = StateContent("", OtherState.RoomHistoryVisibility))),
|
||||
MatrixTimelineItem.Event("m.room.join_rules", anEventTimelineItem(content = StateContent("", OtherState.RoomJoinRules))),
|
||||
MatrixTimelineItem.Event("m.room.pinned_events", anEventTimelineItem(content = StateContent("", OtherState.RoomPinnedEvents))),
|
||||
MatrixTimelineItem.Event("m.room.power_levels", anEventTimelineItem(content = StateContent("", OtherState.RoomPowerLevels))),
|
||||
MatrixTimelineItem.Event("m.room.server_acl", anEventTimelineItem(content = StateContent("", OtherState.RoomServerAcl))),
|
||||
MatrixTimelineItem.Event("m.room.tombstone", anEventTimelineItem(content = StateContent("", OtherState.RoomTombstone))),
|
||||
MatrixTimelineItem.Event("m.space.child", anEventTimelineItem(content = StateContent("", OtherState.SpaceChild))),
|
||||
MatrixTimelineItem.Event("m.space.parent", anEventTimelineItem(content = StateContent("", OtherState.SpaceParent))),
|
||||
MatrixTimelineItem.Event("m.room.policy.rule.room", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleRoom))),
|
||||
MatrixTimelineItem.Event("m.room.policy.rule.server", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleServer))),
|
||||
MatrixTimelineItem.Event("m.room.policy.rule.user", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleUser))),
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
MatrixTimelineItem.Other,
|
||||
MatrixTimelineItem.Virtual("virtual", VirtualTimelineItem.ReadMarker),
|
||||
MatrixTimelineItem.Event("event", anEventTimelineItem()),
|
||||
MatrixTimelineItem.Event("m.room.avatar", anEventTimelineItem(content = StateContent("", OtherState.RoomAvatar("")))),
|
||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(content = StateContent("", OtherState.RoomCreate))),
|
||||
MatrixTimelineItem.Event("m.room.encrypted", anEventTimelineItem(content = StateContent("", OtherState.RoomEncryption))),
|
||||
MatrixTimelineItem.Event("m.room.name", anEventTimelineItem(content = StateContent("", OtherState.RoomName("")))),
|
||||
MatrixTimelineItem.Event("m.room.third_party_invite", anEventTimelineItem(content = StateContent("", OtherState.RoomThirdPartyInvite("")))),
|
||||
MatrixTimelineItem.Event("m.room.topic", anEventTimelineItem(content = StateContent("", OtherState.RoomTopic("")))),
|
||||
MatrixTimelineItem.Event("m.room.custom", anEventTimelineItem(content = StateContent("", OtherState.Custom("")))),
|
||||
)
|
||||
|
||||
val processor = FilterHiddenStateEventsProcessor()
|
||||
|
||||
assertThat(processor.process(items)).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,12 +43,16 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
|||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
||||
class FakeMatrixClient(
|
||||
override val sessionId: SessionId = A_SESSION_ID,
|
||||
override val deviceId: String = "A_DEVICE_ID",
|
||||
override val sessionCoroutineScope: CoroutineScope = TestScope(),
|
||||
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
|
||||
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
|
||||
private val userAvatarUrl: Result<String> = Result.success(AN_AVATAR_URL),
|
||||
override val roomListService: RoomListService = FakeRoomListService(),
|
||||
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(),
|
||||
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||
|
|
@ -135,8 +139,8 @@ class FakeMatrixClient(
|
|||
return userDisplayName
|
||||
}
|
||||
|
||||
override suspend fun loadUserAvatarURLString(): Result<String?> {
|
||||
return userAvatarURLString
|
||||
override suspend fun loadUserAvatarUrl(): Result<String?> {
|
||||
return userAvatarUrl
|
||||
}
|
||||
|
||||
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ fun aBuildMeta(
|
|||
versionName: String = "",
|
||||
versionCode: Int = 0,
|
||||
gitRevision: String = "",
|
||||
gitRevisionDate: String = "",
|
||||
gitBranchName: String = "",
|
||||
flavorDescription: String = "",
|
||||
flavorShortDescription: String = "",
|
||||
|
|
@ -41,7 +40,6 @@ fun aBuildMeta(
|
|||
versionName,
|
||||
versionCode,
|
||||
gitRevision,
|
||||
gitRevisionDate,
|
||||
gitBranchName,
|
||||
flavorDescription,
|
||||
flavorShortDescription
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ class FakeMatrixRoom(
|
|||
override val activeMemberCount: Long = 234L,
|
||||
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
|
||||
canRedact: Boolean = false,
|
||||
canRedactOwn: Boolean = false,
|
||||
canRedactOther: Boolean = false,
|
||||
) : MatrixRoom {
|
||||
private var ignoreResult: Result<Unit> = Result.success(Unit)
|
||||
private var unignoreResult: Result<Unit> = Result.success(Unit)
|
||||
|
|
@ -88,7 +89,8 @@ class FakeMatrixRoom(
|
|||
private var joinRoomResult = Result.success(Unit)
|
||||
private var inviteUserResult = Result.success(Unit)
|
||||
private var canInviteResult = Result.success(true)
|
||||
private var canRedactResult = Result.success(canRedact)
|
||||
private var canRedactOwnResult = Result.success(canRedactOwn)
|
||||
private var canRedactOtherResult = Result.success(canRedactOther)
|
||||
private val canSendStateResults = mutableMapOf<StateEventType, Result<Boolean>>()
|
||||
private val canSendEventResults = mutableMapOf<MessageEventType, Result<Boolean>>()
|
||||
private var sendMediaResult = Result.success(FakeMediaUploadHandler())
|
||||
|
|
@ -113,6 +115,9 @@ class FakeMatrixRoom(
|
|||
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
|
||||
var sendMessageMentions = emptyList<Mention>()
|
||||
val editMessageCalls = mutableListOf<Pair<String, String?>>()
|
||||
private val _typingRecord = mutableListOf<Boolean>()
|
||||
val typingRecord: List<Boolean>
|
||||
get() = _typingRecord
|
||||
|
||||
var sendMediaCount = 0
|
||||
private set
|
||||
|
|
@ -161,7 +166,7 @@ class FakeMatrixRoom(
|
|||
|
||||
private var leaveRoomError: Throwable? = null
|
||||
|
||||
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableStateFlow(aRoomInfo())
|
||||
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableSharedFlow(replay = 1)
|
||||
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
|
||||
|
||||
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
|
||||
|
|
@ -169,9 +174,7 @@ class FakeMatrixRoom(
|
|||
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =
|
||||
MutableStateFlow(MatrixRoomNotificationSettingsState.Unknown)
|
||||
|
||||
override suspend fun updateMembers(): Result<Unit> = simulateLongTask {
|
||||
updateMembersResult
|
||||
}
|
||||
override suspend fun updateMembers() = Unit
|
||||
|
||||
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
|
||||
val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow()
|
||||
|
|
@ -276,8 +279,12 @@ class FakeMatrixRoom(
|
|||
return canInviteResult
|
||||
}
|
||||
|
||||
override suspend fun canUserRedact(userId: UserId): Result<Boolean> {
|
||||
return canRedactResult
|
||||
override suspend fun canUserRedactOwn(userId: UserId): Result<Boolean> {
|
||||
return canRedactOwnResult
|
||||
}
|
||||
|
||||
override suspend fun canUserRedactOther(userId: UserId): Result<Boolean> {
|
||||
return canRedactOtherResult
|
||||
}
|
||||
|
||||
override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result<Boolean> {
|
||||
|
|
@ -422,6 +429,11 @@ class FakeMatrixRoom(
|
|||
progressCallback: ProgressCallback?
|
||||
): Result<MediaUploadHandler> = fakeSendMedia(progressCallback)
|
||||
|
||||
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> {
|
||||
_typingRecord += isTyping
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun generateWidgetWebViewUrl(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
clientId: String,
|
||||
|
|
@ -431,10 +443,6 @@ class FakeMatrixRoom(
|
|||
|
||||
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
|
||||
|
||||
override fun pollHistory(): MatrixTimeline {
|
||||
return FakeMatrixTimeline()
|
||||
}
|
||||
|
||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||
this.leaveRoomError = throwable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.test.room
|
|||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
|
|
@ -34,42 +35,52 @@ fun aRoomSummaryFilled(
|
|||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String = A_ROOM_NAME,
|
||||
isDirect: Boolean = false,
|
||||
avatarURLString: String? = null,
|
||||
avatarUrl: String? = null,
|
||||
lastMessage: RoomMessage? = aRoomMessage(),
|
||||
lastMessageTimestamp: Long? = null,
|
||||
unreadNotificationCount: Int = 2,
|
||||
numUnreadMentions: Int = 1,
|
||||
numUnreadMessages: Int = 2,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
) = RoomSummary.Filled(
|
||||
aRoomSummaryDetail(
|
||||
aRoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
isDirect = isDirect,
|
||||
avatarURLString = avatarURLString,
|
||||
avatarUrl = avatarUrl,
|
||||
lastMessage = lastMessage,
|
||||
lastMessageTimestamp = lastMessageTimestamp,
|
||||
unreadNotificationCount = unreadNotificationCount,
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
notificationMode = notificationMode,
|
||||
)
|
||||
)
|
||||
|
||||
fun aRoomSummaryDetail(
|
||||
fun aRoomSummaryDetails(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String = A_ROOM_NAME,
|
||||
isDirect: Boolean = false,
|
||||
avatarURLString: String? = null,
|
||||
avatarUrl: String? = null,
|
||||
lastMessage: RoomMessage? = aRoomMessage(),
|
||||
lastMessageTimestamp: Long? = null,
|
||||
unreadNotificationCount: Int = 2,
|
||||
numUnreadMentions: Int = 0,
|
||||
numUnreadMessages: Int = 0,
|
||||
numUnreadNotifications: Int = 0,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
inviter: RoomMember? = null,
|
||||
canonicalAlias: String? = null,
|
||||
hasRoomCall: Boolean = false,
|
||||
isDm: Boolean = false,
|
||||
) = RoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
isDirect = isDirect,
|
||||
avatarURLString = avatarURLString,
|
||||
avatarUrl = avatarUrl,
|
||||
lastMessage = lastMessage,
|
||||
lastMessageTimestamp = lastMessageTimestamp,
|
||||
unreadNotificationCount = unreadNotificationCount,
|
||||
notificationMode = notificationMode
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
numUnreadNotifications = numUnreadNotifications,
|
||||
userDefinedNotificationMode = notificationMode,
|
||||
inviter = inviter,
|
||||
canonicalAlias = canonicalAlias,
|
||||
hasRoomCall = hasRoomCall,
|
||||
isDm = isDm,
|
||||
)
|
||||
|
||||
fun aRoomMessage(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class FakeMatrixTimeline(
|
|||
private val _paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState)
|
||||
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = MutableStateFlow(initialTimelineItems)
|
||||
|
||||
var sendReadReceiptCount = 0
|
||||
var sentReadReceipts = mutableListOf<Pair<EventId, ReceiptType>>()
|
||||
private set
|
||||
|
||||
var sendReadReceiptLatch: CompletableDeferred<Unit>? = null
|
||||
|
|
@ -81,7 +81,7 @@ class FakeMatrixTimeline(
|
|||
eventId: EventId,
|
||||
receiptType: ReceiptType,
|
||||
): Result<Unit> = simulateLongTask {
|
||||
sendReadReceiptCount++
|
||||
sentReadReceipts.add(eventId to receiptType)
|
||||
sendReadReceiptLatch?.complete(Unit)
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class FakeCallWidgetSettingsProvider(
|
|||
) : CallWidgetSettingsProvider {
|
||||
val providedBaseUrls = mutableListOf<String>()
|
||||
|
||||
override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings {
|
||||
override fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
|
||||
providedBaseUrls += baseUrl
|
||||
return provideFn(baseUrl, widgetId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* 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.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
|
||||
@Composable
|
||||
fun CheckableMatrixUserRow(
|
||||
checked: Boolean,
|
||||
matrixUser: MatrixUser,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
avatarSize: AvatarSize = AvatarSize.UserListItem,
|
||||
enabled: Boolean = true,
|
||||
) = CheckableUserRow(
|
||||
checked = checked,
|
||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||
name = matrixUser.getBestName(),
|
||||
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||
modifier = modifier,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun CheckableMatrixUserRowPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview {
|
||||
Column {
|
||||
CheckableMatrixUserRow(
|
||||
checked = true,
|
||||
onCheckedChange = { },
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
CheckableMatrixUserRow(
|
||||
checked = false,
|
||||
onCheckedChange = { },
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
CheckableMatrixUserRow(
|
||||
checked = true,
|
||||
onCheckedChange = { },
|
||||
matrixUser = matrixUser,
|
||||
enabled = false,
|
||||
)
|
||||
CheckableMatrixUserRow(
|
||||
checked = false,
|
||||
onCheckedChange = { },
|
||||
matrixUser = matrixUser,
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* 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.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
|
||||
@Composable
|
||||
fun CheckableUnresolvedUserRow(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
avatarData: AvatarData,
|
||||
id: String,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(role = Role.Checkbox, enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
UnresolvedUserRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
avatarData = avatarData,
|
||||
id = id,
|
||||
)
|
||||
|
||||
Checkbox(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun CheckableUnresolvedUserRowPreview() = ElementThemedPreview {
|
||||
val matrixUser = aMatrixUser()
|
||||
Column {
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = false,
|
||||
onCheckedChange = { },
|
||||
avatarData = matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = matrixUser.userId.value,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = true,
|
||||
onCheckedChange = { },
|
||||
avatarData = matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = matrixUser.userId.value,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = false,
|
||||
onCheckedChange = { },
|
||||
avatarData = matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = matrixUser.userId.value,
|
||||
enabled = false,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = true,
|
||||
onCheckedChange = { },
|
||||
avatarData = matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = matrixUser.userId.value,
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,24 +17,29 @@
|
|||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
|
||||
@Composable
|
||||
fun CheckableUserRow(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
avatarData: AvatarData,
|
||||
name: String,
|
||||
subtext: String?,
|
||||
data: CheckableUserRowData,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
|
|
@ -46,19 +51,119 @@ fun CheckableUserRow(
|
|||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
UserRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
avatarData = avatarData,
|
||||
name = name,
|
||||
subtext = subtext,
|
||||
)
|
||||
val rowModifier = Modifier.weight(1f)
|
||||
when (data) {
|
||||
is CheckableUserRowData.Resolved -> {
|
||||
UserRow(
|
||||
modifier = rowModifier,
|
||||
avatarData = data.avatarData,
|
||||
name = data.name,
|
||||
subtext = data.subtext,
|
||||
)
|
||||
}
|
||||
is CheckableUserRowData.Unresolved -> {
|
||||
UnresolvedUserRow(
|
||||
modifier = rowModifier,
|
||||
avatarData = data.avatarData,
|
||||
id = data.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Checkbox(
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface CheckableUserRowData {
|
||||
data class Resolved(
|
||||
val avatarData: AvatarData,
|
||||
val name: String,
|
||||
val subtext: String?,
|
||||
) : CheckableUserRowData
|
||||
|
||||
data class Unresolved(
|
||||
val avatarData: AvatarData,
|
||||
val id: String,
|
||||
) : CheckableUserRowData
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun CheckableResolvedUserRowPreview() = ElementThemedPreview {
|
||||
val matrixUser = aMatrixUser()
|
||||
val data = CheckableUserRowData.Resolved(
|
||||
avatarData = matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
name = matrixUser.displayName.orEmpty(),
|
||||
subtext = matrixUser.userId.value,
|
||||
)
|
||||
Column {
|
||||
CheckableUserRow(
|
||||
checked = false,
|
||||
onCheckedChange = { },
|
||||
data = data,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUserRow(
|
||||
checked = true,
|
||||
onCheckedChange = { },
|
||||
data = data,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUserRow(
|
||||
checked = false,
|
||||
onCheckedChange = { },
|
||||
data = data,
|
||||
enabled = false,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUserRow(
|
||||
checked = true,
|
||||
onCheckedChange = { },
|
||||
data = data,
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun CheckableUnresolvedUserRowPreview() = ElementThemedPreview {
|
||||
val matrixUser = aMatrixUser()
|
||||
val data = CheckableUserRowData.Unresolved(
|
||||
avatarData = matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = matrixUser.userId.value,
|
||||
)
|
||||
Column {
|
||||
CheckableUserRow(
|
||||
checked = false,
|
||||
onCheckedChange = { },
|
||||
data = data,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUserRow(
|
||||
checked = true,
|
||||
onCheckedChange = { },
|
||||
data = data,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUserRow(
|
||||
checked = false,
|
||||
onCheckedChange = { },
|
||||
data = data,
|
||||
enabled = false,
|
||||
)
|
||||
HorizontalDivider()
|
||||
CheckableUserRow(
|
||||
checked = true,
|
||||
onCheckedChange = { },
|
||||
data = data,
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
|
|
@ -60,7 +63,7 @@ fun SelectedRoom(
|
|||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Avatar(AvatarData(roomSummary.roomId.value, roomSummary.name, roomSummary.avatarURLString, AvatarSize.SelectedRoom))
|
||||
Avatar(AvatarData(roomSummary.roomId.value, roomSummary.name, roomSummary.avatarUrl, AvatarSize.SelectedRoom))
|
||||
Text(
|
||||
text = roomSummary.name,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
@ -94,17 +97,37 @@ fun SelectedRoom(
|
|||
@Composable
|
||||
internal fun SelectedRoomPreview() = ElementPreview {
|
||||
SelectedRoom(
|
||||
roomSummary = RoomSummaryDetails(
|
||||
roomId = RoomId("!room:domain"),
|
||||
name = "roomName",
|
||||
canonicalAlias = null,
|
||||
isDirect = true,
|
||||
avatarURLString = null,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
inviter = null,
|
||||
),
|
||||
roomSummary = aRoomSummaryDetails(),
|
||||
onRoomRemoved = {},
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomSummaryDetails(
|
||||
roomId: RoomId = RoomId("!room:domain"),
|
||||
name: String = "roomName",
|
||||
canonicalAlias: String? = null,
|
||||
isDirect: Boolean = true,
|
||||
avatarUrl: String? = null,
|
||||
lastMessage: RoomMessage? = null,
|
||||
inviter: RoomMember? = null,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
hasRoomCall: Boolean = false,
|
||||
isDm: Boolean = false,
|
||||
numUnreadMentions: Int = 0,
|
||||
numUnreadMessages: Int = 0,
|
||||
numUnreadNotifications: Int = 0,
|
||||
) = RoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
canonicalAlias = canonicalAlias,
|
||||
isDirect = isDirect,
|
||||
avatarUrl = avatarUrl,
|
||||
lastMessage = lastMessage,
|
||||
inviter = inviter,
|
||||
userDefinedNotificationMode = notificationMode,
|
||||
hasRoomCall = hasRoomCall,
|
||||
isDm = isDm,
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
numUnreadNotifications = numUnreadNotifications,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.runtime.produceState
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedact
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendMessage
|
||||
|
||||
@Composable
|
||||
|
|
@ -32,8 +33,15 @@ fun MatrixRoom.canSendMessageAsState(type: MessageEventType, updateKey: Long): S
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.canRedactAsState(updateKey: Long): State<Boolean> {
|
||||
fun MatrixRoom.canRedactOwnAsState(updateKey: Long): State<Boolean> {
|
||||
return produceState(initialValue = false, key1 = updateKey) {
|
||||
value = canRedact().getOrElse { false }
|
||||
value = canRedactOwn().getOrElse { false }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.canRedactOtherAsState(updateKey: Long): State<Boolean> {
|
||||
return produceState(initialValue = false, key1 = updateKey) {
|
||||
value = canRedactOther().getOrElse { false }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* 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.ui.room
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.rememberPollHistory(): MatrixTimeline {
|
||||
val pollHistory = remember {
|
||||
pollHistory()
|
||||
}
|
||||
DisposableEffect(pollHistory) {
|
||||
onDispose {
|
||||
pollHistory.close()
|
||||
}
|
||||
}
|
||||
return pollHistory
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="dialog_permission_camera">"Hogy az alkalmazás használhassa a kamerát, adja meg az engedélyt a rendszerbeállításokban."</string>
|
||||
<string name="dialog_permission_generic">"Adja meg az engedélyt a rendszerbeállításokban."</string>
|
||||
<string name="dialog_permission_generic">"Add meg az engedélyt a rendszerbeállításokban."</string>
|
||||
<string name="dialog_permission_microphone">"Hogy az alkalmazás használhassa a mikrofont, adja meg az engedélyt a rendszerbeállításokban."</string>
|
||||
<string name="dialog_permission_notification">"Hogy az alkalmazás megjeleníthesse az értesítéseket, adja meg az engedélyt a rendszerbeállításokban."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="dialog_permission_camera">"Per permettere all\'applicazione di usare la fotocamera, concedi l\'autorizzazione nelle impostazioni di sistema."</string>
|
||||
<string name="dialog_permission_generic">"Concedi l\'autorizzazione nelle impostazioni di sistema."</string>
|
||||
<string name="dialog_permission_microphone">"Per permettere all\'applicazione di usare il microfono, concedi l\'autorizzazione nelle impostazioni di sistema."</string>
|
||||
<string name="dialog_permission_notification">"Per permettere all\'applicazione di mostrare notifiche, concedi l\'autorizzazione nelle impostazioni di sistema."</string>
|
||||
</resources>
|
||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.preferences.api.store
|
|||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PreferencesStore {
|
||||
interface AppPreferencesStore {
|
||||
suspend fun setRichTextEditorEnabled(enabled: Boolean)
|
||||
fun isRichTextEditorEnabledFlow(): Flow<Boolean>
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.features.preferences.api.store
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SessionPreferencesStore {
|
||||
suspend fun setSendPublicReadReceipts(enabled: Boolean)
|
||||
fun isSendPublicReadReceiptsEnabled(): Flow<Boolean>
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -31,6 +31,8 @@ dependencies {
|
|||
api(projects.libraries.preferences.api)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import androidx.datastore.preferences.core.edit
|
|||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
|
|
@ -42,10 +42,10 @@ private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseU
|
|||
private val themeKey = stringPreferencesKey("theme")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPreferencesStore @Inject constructor(
|
||||
class DefaultAppPreferencesStore @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : PreferencesStore {
|
||||
) : AppPreferencesStore {
|
||||
private val store = context.dataStore
|
||||
|
||||
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.preferences.impl.store
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.androidutils.hash.hash
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.File
|
||||
|
||||
class DefaultSessionPreferencesStore(
|
||||
context: Context,
|
||||
sessionId: SessionId,
|
||||
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
|
||||
) : SessionPreferencesStore {
|
||||
companion object {
|
||||
fun storeFile(context: Context, sessionId: SessionId): File {
|
||||
val hashedUserId = sessionId.value.hash().take(16)
|
||||
return context.preferencesDataStoreFile("session_${hashedUserId}_preferences")
|
||||
}
|
||||
}
|
||||
private val sendPublicReadReceiptsKey = booleanPreferencesKey("sendPublicReadReceipts")
|
||||
|
||||
private val dataStoreFile = storeFile(context, sessionId)
|
||||
private val store = PreferenceDataStoreFactory.create(scope = sessionCoroutineScope) { dataStoreFile }
|
||||
|
||||
override suspend fun setSendPublicReadReceipts(enabled: Boolean) = update(sendPublicReadReceiptsKey, enabled)
|
||||
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = get(sendPublicReadReceiptsKey, true)
|
||||
|
||||
override suspend fun clear() {
|
||||
dataStoreFile.safeDelete()
|
||||
}
|
||||
|
||||
private suspend fun <T> update(key: Preferences.Key<T>, value: T) {
|
||||
store.edit { prefs -> prefs[key] = value }
|
||||
}
|
||||
|
||||
private fun <T> get(key: Preferences.Key<T>, default: T): Flow<T> {
|
||||
return store.data.map { prefs -> prefs[key] ?: default }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*
|
||||
* 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.preferences.impl.store
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultSessionPreferencesStoreFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val cache = ConcurrentHashMap<SessionId, DefaultSessionPreferencesStore>()
|
||||
|
||||
fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): DefaultSessionPreferencesStore = cache.getOrPut(sessionId) {
|
||||
DefaultSessionPreferencesStore(context, sessionId, sessionCoroutineScope)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.preferences.impl.store
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Module
|
||||
@ContributesTo(SessionScope::class)
|
||||
object SessionPreferencesModule {
|
||||
@Provides
|
||||
fun providesSessionPreferencesStore(
|
||||
defaultSessionPreferencesStoreFactory: DefaultSessionPreferencesStoreFactory,
|
||||
currentSessionIdHolder: CurrentSessionIdHolder,
|
||||
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
|
||||
): SessionPreferencesStore {
|
||||
return defaultSessionPreferencesStoreFactory
|
||||
.get(currentSessionIdHolder.current, sessionCoroutineScope)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,16 +16,16 @@
|
|||
|
||||
package io.element.android.libraries.featureflag.test
|
||||
|
||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class InMemoryPreferencesStore(
|
||||
class InMemoryAppPreferencesStore(
|
||||
isRichTextEditorEnabled: Boolean = false,
|
||||
isDeveloperModeEnabled: Boolean = false,
|
||||
customElementCallBaseUrl: String? = null,
|
||||
theme: String? = null,
|
||||
) : PreferencesStore {
|
||||
) : AppPreferencesStore {
|
||||
private val isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled)
|
||||
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
||||
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
||||
|
|
@ -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
|
||||
*
|
||||
* 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.featureflag.test
|
||||
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class InMemorySessionPreferencesStore(
|
||||
isSendPublicReadReceiptsEnabled: Boolean = true,
|
||||
) : SessionPreferencesStore {
|
||||
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
||||
var clearCallCount = 0
|
||||
private set
|
||||
|
||||
override suspend fun setSendPublicReadReceipts(enabled: Boolean) {
|
||||
isSendPublicReadReceiptsEnabled.tryEmit(enabled)
|
||||
}
|
||||
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> {
|
||||
return isSendPublicReadReceiptsEnabled
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
clearCallCount++
|
||||
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,10 @@ interface PushService {
|
|||
// TODO Move away
|
||||
fun notificationStyleChanged()
|
||||
|
||||
/**
|
||||
* Return the list of push providers, available at compile time, and
|
||||
* available at runtime, sorted by index.
|
||||
*/
|
||||
fun getAvailablePushProviders(): List<PushProvider>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ class DefaultPushService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun getAvailablePushProviders(): List<PushProvider> {
|
||||
return pushProviders.sortedBy { it.index }
|
||||
return pushProviders
|
||||
.filter { it.isAvailable() }
|
||||
.sortedBy { it.index }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
operation = {
|
||||
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
|
||||
val myUserDisplayName = client.loadUserDisplayName().getOrNull() ?: sessionId.value
|
||||
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
||||
val userAvatarUrl = client.loadUserAvatarUrl().getOrNull()
|
||||
MatrixUser(
|
||||
userId = sessionId,
|
||||
displayName = myUserDisplayName,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ class NotifiableEventResolver @Inject constructor(
|
|||
): NotifiableEvent? {
|
||||
return when (val content = this.content) {
|
||||
is NotificationContent.MessageLike.RoomMessage -> {
|
||||
val messageBody = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value)
|
||||
val senderName = getSenderName(content.senderId)
|
||||
val messageBody = descriptionFromMessageContent(content, senderName)
|
||||
val notificationBody = if (hasMention) {
|
||||
stringProvider.getString(R.string.notification_mentioned_you_body, messageBody)
|
||||
} else {
|
||||
|
|
@ -104,7 +105,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||
eventId = eventId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderName = senderDisplayName,
|
||||
senderName = senderName,
|
||||
body = notificationBody,
|
||||
imageUriString = fetchImageIfPresent(client)?.toString(),
|
||||
roomName = roomDisplayName,
|
||||
|
|
@ -161,7 +162,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||
eventId = eventId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderName = senderDisplayName,
|
||||
senderName = getSenderName(content.senderId),
|
||||
body = stringProvider.getString(CommonStrings.common_poll_summary, content.question),
|
||||
imageUriString = null,
|
||||
roomName = roomDisplayName,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<string name="notification_channel_listening_for_events">"Események figyelése"</string>
|
||||
<string name="notification_channel_noisy">"Zajos értesítések"</string>
|
||||
<string name="notification_channel_silent">"Csendes értesítések"</string>
|
||||
<string name="notification_inline_reply_failed">"** Nem sikerült elküldeni – nyissa meg a szobát"</string>
|
||||
<string name="notification_inline_reply_failed">"** Nem sikerült elküldeni – kérlek nyisd meg a szobát"</string>
|
||||
<string name="notification_invitation_action_join">"Csatlakozás"</string>
|
||||
<string name="notification_invitation_action_reject">"Elutasítás"</string>
|
||||
<string name="notification_invite_body">"Meghívta, hogy csevegjen"</string>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<string name="notification_room_action_mark_as_read">"Megjelölés olvasottként"</string>
|
||||
<string name="notification_room_invite_body">"Meghívta, hogy csatlakozzon a szobához"</string>
|
||||
<string name="notification_sender_me">"Én"</string>
|
||||
<string name="notification_test_push_notification_content">"Az értesítést nézi! Kattintson ide!"</string>
|
||||
<string name="notification_test_push_notification_content">"Az értesítést nézed! Kattints ide!"</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>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s és %2$s"</string>
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
<item quantity="one">"%d szoba"</item>
|
||||
<item quantity="other">"%d szoba"</item>
|
||||
</plurals>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Válassza ki az értesítések fogadásának módját"</string>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Válaszd ki az értesítések fogadásának módját"</string>
|
||||
<string name="push_distributor_background_sync_android">"Háttérszinkronizálás"</string>
|
||||
<string name="push_distributor_firebase_android">"Google szolgáltatások"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően."</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="notification_channel_call">"Chiamata"</string>
|
||||
<string name="notification_channel_silent">"Notifiche silenziose"</string>
|
||||
<string name="notification_inline_reply_failed">"** Invio fallito - si prega di aprire la stanza"</string>
|
||||
<string name="notification_invitation_action_join">"Entra"</string>
|
||||
<string name="notification_invitation_action_reject">"Rifiuta"</string>
|
||||
<string name="notification_invite_body">"Ti ha invitato a chattare"</string>
|
||||
<string name="notification_mentioned_you_body">"Ti ha menzionato: %1$s"</string>
|
||||
<string name="notification_new_messages">"Nuovi messaggi"</string>
|
||||
<string name="notification_reaction_body">"Ha reagito con %1$s"</string>
|
||||
<string name="notification_room_action_mark_as_read">"Segna come letto"</string>
|
||||
<string name="notification_room_invite_body">"Ti ha invitato ad entrare nella stanza"</string>
|
||||
<string name="notification_sender_me">"Io"</string>
|
||||
<string name="notification_test_push_notification_content">"Stai visualizzando la notifica! Cliccami!"</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>
|
||||
<string name="notification_unread_notified_messages_and_invitation">"%1$s e %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room">"%1$s in %2$s"</string>
|
||||
<string name="notification_unread_notified_messages_in_room_and_invitation">"%1$s in %2$s e %3$s"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d messaggio"</item>
|
||||
<item quantity="other">"%1$s: %2$d messaggi"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_compat_summary_title">
|
||||
<item quantity="one">"%d notifica"</item>
|
||||
<item quantity="other">"%d notifiche"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_invitations">
|
||||
<item quantity="one">"%d invito"</item>
|
||||
<item quantity="other">"%d inviti"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
<item quantity="one">"%d nuovo messaggio"</item>
|
||||
<item quantity="other">"%d nuovi messaggi"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages">
|
||||
<item quantity="one">"%d messaggio notificato non letto"</item>
|
||||
<item quantity="other">"%d messaggi notificati non letti"</item>
|
||||
</plurals>
|
||||
<plurals name="notification_unread_notified_messages_in_room_rooms">
|
||||
<item quantity="one">"%d stanza"</item>
|
||||
<item quantity="other">"%d stanze"</item>
|
||||
</plurals>
|
||||
<string name="push_choose_distributor_dialog_title_android">"Scegli come ricevere le notifiche"</string>
|
||||
<string name="push_distributor_background_sync_android">"Sincronizzazione in background"</string>
|
||||
<string name="push_distributor_firebase_android">"Servizi Google"</string>
|
||||
<string name="push_no_valid_google_play_services_apk_android">"Google Play Services non trovato. Le notifiche non funzioneranno bene."</string>
|
||||
<string name="notification_fallback_content">"Notifica"</string>
|
||||
<string name="notification_room_action_quick_reply">"Risposta rapida"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -527,6 +527,7 @@ class NotifiableEventResolverTest {
|
|||
roomId = A_ROOM_ID,
|
||||
senderAvatarUrl = null,
|
||||
senderDisplayName = "Bob",
|
||||
senderIsNameAmbiguous = false,
|
||||
roomAvatarUrl = null,
|
||||
roomDisplayName = null,
|
||||
isDirect = isDirect,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ interface PushProvider {
|
|||
*/
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* Return true if the push provider is available on this device.
|
||||
*/
|
||||
fun isAvailable(): Boolean
|
||||
|
||||
fun getDistributors(): List<Distributor>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,9 +16,13 @@
|
|||
|
||||
package io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
|
|
@ -30,6 +34,7 @@ private val loggerTag = LoggerTag("FirebasePushProvider", LoggerTag.PushLoggerTa
|
|||
|
||||
@ContributesMultibinding(AppScope::class)
|
||||
class FirebasePushProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val firebaseStore: FirebaseStore,
|
||||
private val firebaseTroubleshooter: FirebaseTroubleshooter,
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
|
|
@ -37,6 +42,19 @@ class FirebasePushProvider @Inject constructor(
|
|||
override val index = FirebaseConfig.INDEX
|
||||
override val name = FirebaseConfig.NAME
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
// The PlayServices has to be available
|
||||
val apiAvailability = GoogleApiAvailability.getInstance()
|
||||
val resultCode = apiAvailability.isGooglePlayServicesAvailable(context)
|
||||
return if (resultCode == ConnectionResult.SUCCESS) {
|
||||
Timber.tag(loggerTag.value).d("Google Play Services is available")
|
||||
true
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).w("Google Play Services is not available")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
return listOf(Distributor("Firebase", "Firebase"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.pushproviders.unifiedpush
|
|||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import io.element.android.libraries.androidutils.system.getApplicationLabel
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -26,8 +27,11 @@ import io.element.android.libraries.pushproviders.api.Distributor
|
|||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("UnifiedPushProvider", LoggerTag.PushLoggerTag)
|
||||
|
||||
@ContributesMultibinding(AppScope::class)
|
||||
class UnifiedPushProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
|
|
@ -38,6 +42,17 @@ class UnifiedPushProvider @Inject constructor(
|
|||
override val index = UnifiedPushConfig.INDEX
|
||||
override val name = UnifiedPushConfig.NAME
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
val isAvailable = getDistributors().isNotEmpty()
|
||||
return if (isAvailable) {
|
||||
Timber.tag(loggerTag.value).d("UnifiedPush is available")
|
||||
true
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).w("UnifiedPush is not available")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
val distributors = UnifiedPush.getDistributors(context)
|
||||
return distributors.mapNotNull {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,6 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
*/
|
||||
override fun onUnregistered(context: Context, instance: String) {
|
||||
Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
|
||||
TODO()
|
||||
/*
|
||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||
|
|
|
|||
|
|
@ -29,9 +29,4 @@ interface PushClientSecret {
|
|||
* Return null if not found.
|
||||
*/
|
||||
suspend fun getUserIdFromSecret(clientSecret: String): SessionId?
|
||||
|
||||
/**
|
||||
* To call when the user signs out.
|
||||
*/
|
||||
suspend fun resetSecretForUser(userId: SessionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ anvil {
|
|||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.pushstore.api)
|
||||
|
|
@ -48,6 +49,7 @@ dependencies {
|
|||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
|
||||
androidTestImplementation(libs.coroutines.test)
|
||||
androidTestImplementation(libs.test.core)
|
||||
|
|
|
|||
|
|
@ -23,12 +23,15 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
|
|||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import io.element.android.libraries.androidutils.hash.hash
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushstore.api.UserPushStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Store data related to push about a user.
|
||||
|
|
@ -37,7 +40,24 @@ class UserPushStoreDataStore(
|
|||
private val context: Context,
|
||||
userId: SessionId,
|
||||
) : UserPushStore {
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_store_$userId")
|
||||
// Hash the sessionId to get rid of exotic chars and take only the first 16 chars.
|
||||
// The risk of collision is not high.
|
||||
private val preferenceName = "push_store_${userId.value.hash().take(16)}"
|
||||
|
||||
init {
|
||||
// Migrate legacy data. Previous file can be too long if the userId is too long. The userId can be up to 255 chars.
|
||||
// Example of long file path, with `averylonguserid` replacing a very longer name
|
||||
// /data/user/0/io.element.android.x.debug/files/datastore/push_store_@averylonguserid:example.org.preferences_pb
|
||||
val legacyFile = context.preferencesDataStoreFile("push_store_$userId")
|
||||
if (legacyFile.exists()) {
|
||||
Timber.d("Migrating legacy push data store for $userId")
|
||||
if (!legacyFile.renameTo(context.preferencesDataStoreFile(preferenceName))) {
|
||||
Timber.w("Failed to migrate legacy push data store for $userId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = preferenceName)
|
||||
private val pushProviderName = stringPreferencesKey("pushProviderName")
|
||||
private val currentPushKey = stringPreferencesKey("currentPushKey")
|
||||
private val notificationEnabled = booleanPreferencesKey("notificationEnabled")
|
||||
|
|
@ -80,5 +100,7 @@ class UserPushStoreDataStore(
|
|||
context.dataStore.edit {
|
||||
it.clear()
|
||||
}
|
||||
// Also delete the file
|
||||
context.preferencesDataStoreFile(preferenceName).delete()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,17 +18,26 @@ package io.element.android.libraries.pushstore.impl.clientsecret
|
|||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class, boundType = PushClientSecret::class)
|
||||
class PushClientSecretImpl @Inject constructor(
|
||||
private val pushClientSecretFactory: PushClientSecretFactory,
|
||||
private val pushClientSecretStore: PushClientSecretStore,
|
||||
) : PushClientSecret {
|
||||
private val sessionObserver: SessionObserver,
|
||||
) : PushClientSecret, SessionListener {
|
||||
init {
|
||||
observeSessions()
|
||||
}
|
||||
|
||||
override suspend fun getSecretForUser(userId: SessionId): String {
|
||||
val existingSecret = pushClientSecretStore.getSecret(userId)
|
||||
if (existingSecret != null) {
|
||||
|
|
@ -43,7 +52,16 @@ class PushClientSecretImpl @Inject constructor(
|
|||
return pushClientSecretStore.getUserIdFromSecret(clientSecret)
|
||||
}
|
||||
|
||||
override suspend fun resetSecretForUser(userId: SessionId) {
|
||||
pushClientSecretStore.resetSecret(userId)
|
||||
private fun observeSessions() {
|
||||
sessionObserver.addListener(this)
|
||||
}
|
||||
|
||||
override suspend fun onSessionCreated(userId: String) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
override suspend fun onSessionDeleted(userId: String) {
|
||||
// Delete the secret
|
||||
pushClientSecretStore.resetSecret(SessionId(userId))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.libraries.pushstore.impl.clientsecret
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ internal class PushClientSecretImplTest {
|
|||
fun test() = runTest {
|
||||
val factory = FakePushClientSecretFactory()
|
||||
val store = InMemoryPushClientSecretStore()
|
||||
val sut = PushClientSecretImpl(factory, store)
|
||||
val sut = PushClientSecretImpl(factory, store, NoOpSessionObserver())
|
||||
|
||||
val secret0 = factory.getSecretForUser(0)
|
||||
val secret1 = factory.getSecretForUser(1)
|
||||
|
|
@ -56,7 +57,7 @@ internal class PushClientSecretImplTest {
|
|||
assertThat(sut.getUserIdFromSecret(A_UNKNOWN_SECRET)).isNull()
|
||||
|
||||
// User signs out
|
||||
sut.resetSecretForUser(A_USER_ID_0)
|
||||
sut.onSessionDeleted(A_USER_ID_0.value)
|
||||
assertThat(store.getSecrets()).hasSize(1)
|
||||
// Create a new secret after reset
|
||||
assertThat(sut.getSecretForUser(A_USER_ID_0)).isEqualTo(secret2)
|
||||
|
|
|
|||
|
|
@ -19,9 +19,8 @@ package io.element.android.libraries.roomselect.impl
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -41,7 +40,7 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
|
|||
resultState = SearchBarResultState.Results(aForwardMessagesRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain")))
|
||||
selectedRooms = persistentListOf(aRoomSummaryDetails(roomId = RoomId("!room2:domain")))
|
||||
),
|
||||
// Add other states here
|
||||
)
|
||||
|
|
@ -62,32 +61,10 @@ private fun aRoomSelectState(
|
|||
)
|
||||
|
||||
private fun aForwardMessagesRoomList() = persistentListOf(
|
||||
aRoomDetailsState(),
|
||||
aRoomDetailsState(
|
||||
aRoomSummaryDetails(),
|
||||
aRoomSummaryDetails(
|
||||
roomId = RoomId("!room2:domain"),
|
||||
name = "Room with alias",
|
||||
canonicalAlias = "#alias:example.org",
|
||||
),
|
||||
)
|
||||
|
||||
private fun aRoomDetailsState(
|
||||
roomId: RoomId = RoomId("!room:domain"),
|
||||
name: String = "roomName",
|
||||
canonicalAlias: String? = null,
|
||||
isDirect: Boolean = true,
|
||||
avatarURLString: String? = null,
|
||||
lastMessage: RoomMessage? = null,
|
||||
lastMessageTimestamp: Long? = null,
|
||||
unreadNotificationCount: Int = 0,
|
||||
inviter: RoomMember? = null,
|
||||
) = RoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
canonicalAlias = canonicalAlias,
|
||||
isDirect = isDirect,
|
||||
avatarURLString = avatarURLString,
|
||||
lastMessage = lastMessage,
|
||||
lastMessageTimestamp = lastMessageTimestamp,
|
||||
unreadNotificationCount = unreadNotificationCount,
|
||||
inviter = inviter,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ private fun RoomSummaryView(
|
|||
avatarData = AvatarData(
|
||||
id = summary.roomId.value,
|
||||
name = summary.name,
|
||||
url = summary.avatarURLString,
|
||||
url = summary.avatarUrl,
|
||||
size = AvatarSize.RoomSelectRoomListItem,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
@ -72,7 +72,7 @@ class RoomSelectPresenterTests {
|
|||
@Test
|
||||
fun `present - update query`() = runTest {
|
||||
val roomListService = FakeRoomListService().apply {
|
||||
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
|
||||
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetails())))
|
||||
}
|
||||
val client = FakeMatrixClient(roomListService = roomListService)
|
||||
val presenter = aPresenter(client = client)
|
||||
|
|
@ -80,7 +80,7 @@ class RoomSelectPresenterTests {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetail())))
|
||||
assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetails())))
|
||||
|
||||
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
|
||||
assertThat(awaitItem().query).isEqualTo("string not contained")
|
||||
|
|
@ -96,7 +96,7 @@ class RoomSelectPresenterTests {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
skipItems(1)
|
||||
val summary = aRoomSummaryDetail()
|
||||
val summary = aRoomSummaryDetails()
|
||||
|
||||
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary))
|
||||
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
|
||||
|
|
|
|||
|
|
@ -29,4 +29,5 @@ data class SessionData(
|
|||
val loginTimestamp: Date?,
|
||||
val isTokenValid: Boolean,
|
||||
val loginType: LoginType,
|
||||
val passphrase: String?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ 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.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -37,6 +39,8 @@ class DatabaseSessionStore @Inject constructor(
|
|||
private val database: SessionDatabase,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : SessionStore {
|
||||
private val sessionDataMutex = Mutex()
|
||||
|
||||
override fun isLoggedIn(): Flow<LoggedInState> {
|
||||
return database.sessionDataQueries.selectFirst()
|
||||
.asFlow()
|
||||
|
|
@ -53,11 +57,11 @@ class DatabaseSessionStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun storeData(sessionData: SessionData) {
|
||||
override suspend fun storeData(sessionData: SessionData) = sessionDataMutex.withLock {
|
||||
database.sessionDataQueries.insertSessionData(sessionData.toDbModel())
|
||||
}
|
||||
|
||||
override suspend fun updateData(sessionData: SessionData) {
|
||||
override suspend fun updateData(sessionData: SessionData) = sessionDataMutex.withLock {
|
||||
val result = database.sessionDataQueries.selectByUserId(sessionData.userId)
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
|
|
@ -66,8 +70,7 @@ class DatabaseSessionStore @Inject constructor(
|
|||
Timber.e("User ${sessionData.userId} not found in session database")
|
||||
return
|
||||
}
|
||||
|
||||
// Copy new data from SDK, but keep login timestamp
|
||||
// Copy new data from SDK, but keep login timestamp
|
||||
database.sessionDataQueries.updateSession(
|
||||
sessionData.copy(
|
||||
loginTimestamp = result.loginTimestamp,
|
||||
|
|
@ -76,21 +79,27 @@ class DatabaseSessionStore @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun getLatestSession(): SessionData? {
|
||||
return database.sessionDataQueries.selectFirst()
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
return sessionDataMutex.withLock {
|
||||
database.sessionDataQueries.selectFirst()
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSession(sessionId: String): SessionData? {
|
||||
return database.sessionDataQueries.selectByUserId(sessionId)
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
return sessionDataMutex.withLock {
|
||||
database.sessionDataQueries.selectByUserId(sessionId)
|
||||
.executeAsOneOrNull()
|
||||
?.toApiModel()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAllSessions(): List<SessionData> {
|
||||
return database.sessionDataQueries.selectAll()
|
||||
.executeAsList()
|
||||
.map { it.toApiModel() }
|
||||
return sessionDataMutex.withLock {
|
||||
database.sessionDataQueries.selectAll()
|
||||
.executeAsList()
|
||||
.map { it.toApiModel() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun sessionsFlow(): Flow<List<SessionData>> {
|
||||
|
|
@ -102,6 +111,8 @@ class DatabaseSessionStore @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun removeSession(sessionId: String) {
|
||||
database.sessionDataQueries.removeSession(sessionId)
|
||||
sessionDataMutex.withLock {
|
||||
database.sessionDataQueries.removeSession(sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
|||
loginTimestamp = loginTimestamp?.time,
|
||||
isTokenValid = if (isTokenValid) 1L else 0L,
|
||||
loginType = loginType.name,
|
||||
passphrase = passphrase,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -48,5 +49,6 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
|||
loginTimestamp = loginTimestamp?.let { Date(it) },
|
||||
isTokenValid = isTokenValid == 1L,
|
||||
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
|
||||
passphrase = passphrase,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -21,7 +21,9 @@ CREATE TABLE SessionData (
|
|||
oidcData TEXT,
|
||||
-- added in version 4
|
||||
isTokenValid INTEGER NOT NULL DEFAULT 1,
|
||||
loginType TEXT
|
||||
loginType TEXT,
|
||||
-- added in version 5
|
||||
passphrase TEXT
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue