Merge branch 'develop' into feature/fga/join_space
This commit is contained in:
commit
c4308e9810
446 changed files with 5669 additions and 2617 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -33,12 +34,7 @@ dependencies {
|
|||
implementation(libs.androidx.datastore.preferences)
|
||||
api(libs.androidx.browser)
|
||||
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -25,7 +26,5 @@ dependencies {
|
|||
api(libs.androidx.lifecycle.runtime)
|
||||
api(libs.molecule.runtime)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
||||
interface AssistedNodeFactory<NODE : Node> {
|
||||
fun interface AssistedNodeFactory<NODE : Node> {
|
||||
fun create(buildContext: BuildContext, plugins: List<Plugin>): NODE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ interface FeatureEntryPoint
|
|||
/**
|
||||
* Can be used when the feature only exposes a simple node without the need of plugins.
|
||||
*/
|
||||
interface SimpleFeatureEntryPoint : FeatureEntryPoint {
|
||||
fun interface SimpleFeatureEntryPoint : FeatureEntryPoint {
|
||||
fun createNode(parentNode: Node, buildContext: BuildContext): Node
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
package io.element.android.libraries.architecture
|
||||
|
||||
import android.content.Context
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
|
@ -22,17 +21,9 @@ inline fun <reified N : Node> Node.createNode(
|
|||
return bindings.createNode(buildContext, plugins)
|
||||
}
|
||||
|
||||
inline fun <reified N : Node> Context.createNode(
|
||||
buildContext: BuildContext,
|
||||
plugins: List<Plugin> = emptyList()
|
||||
): N {
|
||||
val bindings: NodeFactoriesBindings = bindings()
|
||||
return bindings.createNode(buildContext, plugins)
|
||||
}
|
||||
|
||||
inline fun <reified N : Node> NodeFactoriesBindings.createNode(
|
||||
buildContext: BuildContext,
|
||||
plugins: List<Plugin> = emptyList()
|
||||
plugins: List<Plugin>,
|
||||
): N {
|
||||
val nodeClass = N::class
|
||||
val nodeFactoryMap = nodeFactories()
|
||||
|
|
@ -46,8 +37,7 @@ inline fun <reified N : Node> NodeFactoriesBindings.createNode(
|
|||
return node as N
|
||||
}
|
||||
|
||||
// @BindingContainer
|
||||
interface NodeFactoriesBindings {
|
||||
fun interface NodeFactoriesBindings {
|
||||
@Multibinds
|
||||
fun nodeFactories(): Map<KClass<out Node>, AssistedNodeFactory<*>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -21,6 +22,5 @@ dependencies {
|
|||
implementation(projects.libraries.di)
|
||||
api(projects.libraries.cryptography.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -13,7 +15,6 @@ android {
|
|||
namespace = "io.element.android.libraries.dateformatter.api"
|
||||
|
||||
dependencies {
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -40,13 +41,8 @@ android {
|
|||
api(projects.libraries.dateformatter.api)
|
||||
api(libs.datetime)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ package io.element.android.libraries.deeplink.api.usecase
|
|||
|
||||
import android.app.Activity
|
||||
|
||||
interface InviteFriendsUseCase {
|
||||
fun interface InviteFriendsUseCase {
|
||||
fun execute(activity: Activity)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -28,9 +29,6 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -42,10 +44,6 @@ android {
|
|||
ksp(libs.showkase.processor)
|
||||
implementation(libs.showkase)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,18 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
|||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A progress dialog, with a spinner, and optional text content.
|
||||
*
|
||||
* @param modifier
|
||||
* @param text Optional text to show under the spinner.
|
||||
* @param type
|
||||
* @param properties
|
||||
* @param showCancelButton
|
||||
* @param onDismissRequest
|
||||
* @param content Optional additional content to show under the spinner, and above the cancel button (if shown). If both `text` and `content` are supplied,
|
||||
* `text` is shown above `content`.
|
||||
*/
|
||||
@Composable
|
||||
fun ProgressDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -46,6 +58,7 @@ fun ProgressDialog(
|
|||
properties: DialogProperties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
showCancelButton: Boolean = false,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
|
|
@ -75,7 +88,8 @@ fun ProgressDialog(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +110,8 @@ private fun ProgressDialogContent(
|
|||
CircularProgressIndicator(
|
||||
color = ElementTheme.colors.iconPrimary
|
||||
)
|
||||
}
|
||||
},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -118,6 +133,7 @@ private fun ProgressDialogContent(
|
|||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
content()
|
||||
if (showCancelButton) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Box(
|
||||
|
|
@ -138,7 +154,7 @@ private fun ProgressDialogContent(
|
|||
@Composable
|
||||
internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
||||
DialogPreview {
|
||||
ProgressDialogContent(text = "test dialog content", showCancelButton = true)
|
||||
ProgressDialogContent(text = "test dialog content", showCancelButton = true, content = {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,3 +163,34 @@ internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
|||
internal fun ProgressDialogPreview() = ElementPreview {
|
||||
ProgressDialog(text = "test dialog content", showCancelButton = true)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ProgressDialogWithContentPreview() = ElementPreview {
|
||||
ProgressDialog(showCancelButton = true) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Heading",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Subtext",
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ProgressDialogWithTextAndContentPreview() = ElementPreview {
|
||||
ProgressDialog(text = "Text Content") {
|
||||
Text(
|
||||
text = "blah blah",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ internal fun SimpleAlertDialogContent(
|
|||
Text(
|
||||
text = titleText,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
textAlign = if (icon != null) TextAlign.Center else TextAlign.Start,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -510,3 +510,43 @@ internal fun DialogWithThirdButtonPreview() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Dialogs, name = "Dialog with a very long title")
|
||||
@Composable
|
||||
@Suppress("MaxLineLength")
|
||||
internal fun DialogWithVeryLongTitlePreview() {
|
||||
ElementThemedPreview(showBackground = false) {
|
||||
DialogPreview {
|
||||
SimpleAlertDialogContent(
|
||||
title = "Dialog Title that takes more than one line",
|
||||
content = "A dialog is a type of modal window that appears in front of app content to provide critical information," +
|
||||
" or prompt for a decision to be made. Learn more",
|
||||
submitText = "OK",
|
||||
onSubmitClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Dialogs, name = "Dialog with a very long title and icon")
|
||||
@Composable
|
||||
@Suppress("MaxLineLength")
|
||||
internal fun DialogWithVeryLongTitleAndIconPreview() {
|
||||
ElementThemedPreview(showBackground = false) {
|
||||
DialogPreview {
|
||||
SimpleAlertDialogContent(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.NotificationsSolid(),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
title = "Dialog Title that takes more than one line",
|
||||
content = "A dialog is a type of modal window that appears in front of app content to provide critical information," +
|
||||
" or prompt for a decision to be made. Learn more",
|
||||
submitText = "OK",
|
||||
onSubmitClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -32,9 +33,7 @@ dependencies {
|
|||
implementation(projects.services.toolbox.api)
|
||||
api(projects.libraries.eventformatter.api)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.services.toolbox.impl)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ class DefaultBaseRoomLastMessageFormatterTest {
|
|||
val info = ImageInfo(null, null, null, null, null, null, null)
|
||||
val message = createRoomEvent(false, null, aStickerContent(body, info, aMediaSource(url = "url")))
|
||||
val result = formatter.format(message, false)
|
||||
val expectedBody = someoneElseId.toString() + ": Sticker (a sticker body)"
|
||||
val expectedBody = someoneElseId.value + ": Sticker (a sticker body)"
|
||||
assertThat(result.toString()).isEqualTo(expectedBody)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -27,9 +28,7 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(libs.coroutines.core)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
|
|
@ -27,19 +28,10 @@ dependencies {
|
|||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.testtags)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -26,11 +27,7 @@ dependencies {
|
|||
|
||||
api(projects.libraries.indicator.api)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.truth)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -54,7 +55,6 @@ dependencies {
|
|||
implementation(libs.coroutines.core)
|
||||
api(projects.libraries.architecture)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,11 +156,6 @@ interface MatrixClient {
|
|||
*/
|
||||
suspend fun currentSlidingSyncVersion(): Result<SlidingSyncVersion>
|
||||
|
||||
/**
|
||||
* Returns the available sliding sync versions for the current user.
|
||||
*/
|
||||
suspend fun availableSlidingSyncVersions(): Result<List<SlidingSyncVersion>>
|
||||
|
||||
fun canDeactivateAccount(): Boolean
|
||||
suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,9 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession
|
|||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface MatrixAuthenticationService {
|
||||
fun loggedInStateFlow(): Flow<LoggedInState>
|
||||
suspend fun getLatestSessionId(): SessionId?
|
||||
|
||||
/**
|
||||
* Restore a session from a [sessionId].
|
||||
* Do not restore anything it the access token is not valid anymore.
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ object MatrixPatterns {
|
|||
val urlMatch = match.groupValues[1]
|
||||
when (val permalink = permalinkParser.parse(urlMatch)) {
|
||||
is PermalinkData.UserLink -> {
|
||||
add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.toString(), match.range.first, match.range.last + 1))
|
||||
add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.value, match.range.first, match.range.last + 1))
|
||||
}
|
||||
is PermalinkData.RoomLink -> {
|
||||
when (permalink.roomIdOrAlias) {
|
||||
|
|
|
|||
|
|
@ -49,9 +49,10 @@ sealed interface NotificationContent {
|
|||
val senderId: UserId,
|
||||
) : MessageLike
|
||||
|
||||
data class CallNotify(
|
||||
data class RtcNotification(
|
||||
val senderId: UserId,
|
||||
val type: CallNotifyType,
|
||||
val type: RtcNotificationType,
|
||||
val expirationTimestampMillis: Long
|
||||
) : MessageLike
|
||||
|
||||
data object CallHangup : MessageLike
|
||||
|
|
@ -118,7 +119,7 @@ sealed interface NotificationContent {
|
|||
) : NotificationContent
|
||||
}
|
||||
|
||||
enum class CallNotifyType {
|
||||
enum class RtcNotificationType {
|
||||
RING,
|
||||
NOTIFY
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
|
|||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.Closeable
|
||||
|
||||
|
|
@ -239,7 +240,11 @@ interface BaseRoom : Closeable {
|
|||
*/
|
||||
suspend fun reportRoom(reason: String?): Result<Unit>
|
||||
|
||||
/**
|
||||
suspend fun declineCall(notificationEventId: EventId): Result<Unit>
|
||||
|
||||
suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId>
|
||||
|
||||
/**
|
||||
* Destroy the room and release all resources associated to it.
|
||||
*/
|
||||
fun destroy()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ enum class MessageEventType {
|
|||
CALL_INVITE,
|
||||
CALL_HANGUP,
|
||||
CALL_CANDIDATES,
|
||||
CALL_NOTIFY,
|
||||
RTC_NOTIFICATION,
|
||||
KEY_VERIFICATION_READY,
|
||||
KEY_VERIFICATION_START,
|
||||
KEY_VERIFICATION_CANCEL,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ data class RoomMember(
|
|||
val membership: RoomMembershipState,
|
||||
val isNameAmbiguous: Boolean,
|
||||
val powerLevel: Long,
|
||||
val normalizedPowerLevel: Long,
|
||||
val isIgnored: Boolean,
|
||||
val role: Role,
|
||||
val membershipChangeReason: String?,
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ interface Timeline : AutoCloseable {
|
|||
|
||||
suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result<Unit>
|
||||
|
||||
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit>
|
||||
suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Boolean>
|
||||
|
||||
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@ object EventType {
|
|||
|
||||
// Call Events
|
||||
const val CALL_INVITE = "m.call.invite"
|
||||
const val CALL_NOTIFY = "m.call.notify"
|
||||
|
||||
const val RTC_NOTIFICATION = "org.matrix.msc4075.rtc.notification"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,9 @@ import dev.zacsweers.metro.Inject
|
|||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
@SingleIn(SessionScope::class)
|
||||
@Inject
|
||||
class CurrentSessionIdHolder(matrixClient: MatrixClient) {
|
||||
val current = matrixClient.sessionId
|
||||
|
||||
fun isCurrentSession(sessionId: SessionId?): Boolean = current == sessionId
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@ interface CallWidgetSettingsProvider {
|
|||
widgetId: String = UUID.randomUUID().toString(),
|
||||
encrypted: Boolean,
|
||||
direct: Boolean,
|
||||
hasActiveCall: Boolean,
|
||||
): MatrixWidgetSettings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -41,9 +42,7 @@ dependencies {
|
|||
implementation(libs.serialization.json)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
|
|
@ -51,7 +50,4 @@ dependencies {
|
|||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.turbine)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,7 +287,6 @@ class RustMatrixClient(
|
|||
}
|
||||
|
||||
override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) {
|
||||
innerClient.rooms()
|
||||
roomFactory.getBaseRoom(roomId)
|
||||
}
|
||||
|
||||
|
|
@ -689,12 +688,6 @@ class RustMatrixClient(
|
|||
})
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
||||
override suspend fun availableSlidingSyncVersions(): Result<List<SlidingSyncVersion>> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.availableSlidingSyncVersions().map { it.map() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun currentSlidingSyncVersion(): Result<SlidingSyncVersion> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.session().slidingSyncVersion.map()
|
||||
|
|
|
|||
|
|
@ -33,11 +33,9 @@ import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
|
|||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -83,14 +81,6 @@ class RustMatrixAuthenticationService(
|
|||
.also { sessionPaths = it }
|
||||
}
|
||||
|
||||
override fun loggedInStateFlow(): Flow<LoggedInState> {
|
||||
return sessionStore.isLoggedIn()
|
||||
}
|
||||
|
||||
override suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) {
|
||||
sessionStore.getLatestSession()?.userId?.let { SessionId(it) }
|
||||
}
|
||||
|
||||
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> = withContext(coroutineDispatchers.io) {
|
||||
runCatchingExceptions {
|
||||
val sessionData = sessionStore.getSession(sessionId.value)
|
||||
|
|
@ -158,7 +148,7 @@ class RustMatrixAuthenticationService(
|
|||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.storeData(sessionData)
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
// Clean up the strong reference held here since it's no longer necessary
|
||||
currentClient = null
|
||||
|
|
@ -182,7 +172,7 @@ class RustMatrixAuthenticationService(
|
|||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
clear()
|
||||
sessionStore.storeData(sessionData)
|
||||
sessionStore.addSession(sessionData)
|
||||
SessionId(sessionData.userId)
|
||||
}
|
||||
}
|
||||
|
|
@ -250,7 +240,7 @@ class RustMatrixAuthenticationService(
|
|||
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.storeData(sessionData)
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
// Clean up the strong reference held here since it's no longer necessary
|
||||
currentClient = null
|
||||
|
|
@ -295,7 +285,7 @@ class RustMatrixAuthenticationService(
|
|||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client)
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.storeData(sessionData)
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
// Clean up the strong reference held here since it's no longer necessary
|
||||
currentClient = null
|
||||
|
|
|
|||
|
|
@ -10,16 +10,16 @@ package io.element.android.libraries.matrix.impl.notification
|
|||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
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.NotifyType
|
||||
import org.matrix.rustcomponents.sdk.StateEventContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineEvent
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventType
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.RtcNotificationType as SdkRtcNotificationType
|
||||
|
||||
class TimelineEventToNotificationContentMapper {
|
||||
fun map(timelineEvent: TimelineEvent): Result<NotificationContent> {
|
||||
|
|
@ -78,7 +78,11 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
|
|||
MessageLikeEventContent.CallCandidates -> NotificationContent.MessageLike.CallCandidates
|
||||
MessageLikeEventContent.CallHangup -> NotificationContent.MessageLike.CallHangup
|
||||
MessageLikeEventContent.CallInvite -> NotificationContent.MessageLike.CallInvite(senderId)
|
||||
is MessageLikeEventContent.CallNotify -> NotificationContent.MessageLike.CallNotify(senderId, notifyType.map())
|
||||
is MessageLikeEventContent.RtcNotification -> NotificationContent.MessageLike.RtcNotification(
|
||||
senderId = senderId,
|
||||
type = notificationType.map(),
|
||||
expirationTimestampMillis = expirationTs.toLong()
|
||||
)
|
||||
MessageLikeEventContent.KeyVerificationAccept -> NotificationContent.MessageLike.KeyVerificationAccept
|
||||
MessageLikeEventContent.KeyVerificationCancel -> NotificationContent.MessageLike.KeyVerificationCancel
|
||||
MessageLikeEventContent.KeyVerificationDone -> NotificationContent.MessageLike.KeyVerificationDone
|
||||
|
|
@ -101,7 +105,7 @@ private fun MessageLikeEventContent.toContent(senderId: UserId): NotificationCon
|
|||
}
|
||||
}
|
||||
|
||||
private fun NotifyType.map(): CallNotifyType = when (this) {
|
||||
NotifyType.NOTIFY -> CallNotifyType.NOTIFY
|
||||
NotifyType.RING -> CallNotifyType.RING
|
||||
private fun SdkRtcNotificationType.map(): RtcNotificationType = when (this) {
|
||||
SdkRtcNotificationType.NOTIFICATION -> RtcNotificationType.NOTIFY
|
||||
SdkRtcNotificationType.RING -> RtcNotificationType.RING
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ fun MessageEventType.map(): MessageLikeEventType = when (this) {
|
|||
MessageEventType.CALL_INVITE -> MessageLikeEventType.CALL_INVITE
|
||||
MessageEventType.CALL_HANGUP -> MessageLikeEventType.CALL_HANGUP
|
||||
MessageEventType.CALL_CANDIDATES -> MessageLikeEventType.CALL_CANDIDATES
|
||||
MessageEventType.CALL_NOTIFY -> MessageLikeEventType.CALL_NOTIFY
|
||||
MessageEventType.RTC_NOTIFICATION -> MessageLikeEventType.RTC_NOTIFICATION
|
||||
MessageEventType.KEY_VERIFICATION_READY -> MessageLikeEventType.KEY_VERIFICATION_READY
|
||||
MessageEventType.KEY_VERIFICATION_START -> MessageLikeEventType.KEY_VERIFICATION_START
|
||||
MessageEventType.KEY_VERIFICATION_CANCEL -> MessageLikeEventType.KEY_VERIFICATION_CANCEL
|
||||
|
|
@ -41,7 +41,7 @@ fun MessageLikeEventType.map(): MessageEventType = when (this) {
|
|||
MessageLikeEventType.CALL_INVITE -> MessageEventType.CALL_INVITE
|
||||
MessageLikeEventType.CALL_HANGUP -> MessageEventType.CALL_HANGUP
|
||||
MessageLikeEventType.CALL_CANDIDATES -> MessageEventType.CALL_CANDIDATES
|
||||
MessageLikeEventType.CALL_NOTIFY -> MessageEventType.CALL_NOTIFY
|
||||
MessageLikeEventType.RTC_NOTIFICATION -> MessageEventType.RTC_NOTIFICATION
|
||||
MessageLikeEventType.KEY_VERIFICATION_READY -> MessageEventType.KEY_VERIFICATION_READY
|
||||
MessageLikeEventType.KEY_VERIFICATION_START -> MessageEventType.KEY_VERIFICATION_START
|
||||
MessageLikeEventType.KEY_VERIFICATION_CANCEL -> MessageEventType.KEY_VERIFICATION_CANCEL
|
||||
|
|
|
|||
|
|
@ -38,10 +38,12 @@ import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
|
|||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.CallDeclineListener
|
||||
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
|
|
@ -300,4 +302,20 @@ class RustBaseRoom(
|
|||
innerRoom.reportRoom(reason.orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun declineCall(notificationEventId: EventId): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.declineCall(notificationEventId.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId> = withContext(roomDispatcher) {
|
||||
mxCallbackFlow {
|
||||
innerRoom.subscribeToCallDeclineEvents(notificationEventId.value, object : CallDeclineListener {
|
||||
override fun call(declinerUserId: String) {
|
||||
trySend(UserId(declinerUserId))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ fun RustAllowRule.map(): AllowRule {
|
|||
|
||||
fun AllowRule.map(): RustAllowRule {
|
||||
return when (this) {
|
||||
is AllowRule.RoomMembership -> RustAllowRule.RoomMembership(roomId.toString())
|
||||
is AllowRule.RoomMembership -> RustAllowRule.RoomMembership(roomId.value)
|
||||
is AllowRule.Custom -> RustAllowRule.Custom(json)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ object RoomMemberMapper {
|
|||
membership = mapMembership(roomMember.membership),
|
||||
isNameAmbiguous = roomMember.isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = roomMember.normalizedPowerLevel.into(),
|
||||
isIgnored = roomMember.isIgnored,
|
||||
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
|
||||
membershipChangeReason = roomMember.membershipChangeReason
|
||||
|
|
|
|||
|
|
@ -431,7 +431,7 @@ class RustTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = withContext(dispatcher) {
|
||||
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Boolean> = withContext(dispatcher) {
|
||||
runCatchingExceptions {
|
||||
inner.toggleReaction(
|
||||
key = emoji,
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ class TimelineEventContentMapper(
|
|||
)
|
||||
}
|
||||
is TimelineItemContent.CallInvite -> LegacyCallInviteContent
|
||||
is TimelineItemContent.CallNotify -> CallNotifyContent
|
||||
is TimelineItemContent.RtcNotification -> CallNotifyContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
|||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk.EncryptionSystem
|
||||
import uniffi.matrix_sdk.HeaderStyle
|
||||
import uniffi.matrix_sdk.NotificationType
|
||||
import uniffi.matrix_sdk.VirtualElementCallWidgetOptions
|
||||
import uniffi.matrix_sdk.VirtualElementCallWidgetConfig
|
||||
import uniffi.matrix_sdk.VirtualElementCallWidgetProperties
|
||||
import uniffi.matrix_sdk.Intent as CallIntent
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
|
|
@ -31,19 +31,14 @@ class DefaultCallWidgetSettingsProvider(
|
|||
private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : CallWidgetSettingsProvider {
|
||||
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean): MatrixWidgetSettings {
|
||||
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean, hasActiveCall: Boolean): MatrixWidgetSettings {
|
||||
val isAnalyticsEnabled = analyticsService.userConsentFlow.first()
|
||||
val options = VirtualElementCallWidgetOptions(
|
||||
val properties = VirtualElementCallWidgetProperties(
|
||||
elementCallUrl = baseUrl,
|
||||
widgetId = widgetId,
|
||||
preload = null,
|
||||
fontScale = null,
|
||||
appPrompt = false,
|
||||
confineToRoom = true,
|
||||
font = null,
|
||||
encryption = if (encrypted) EncryptionSystem.PerParticipantKeys else EncryptionSystem.Unencrypted,
|
||||
intent = CallIntent.START_CALL,
|
||||
hideScreensharing = false,
|
||||
posthogUserId = callAnalyticsCredentialsProvider.posthogUserId.takeIf { isAnalyticsEnabled },
|
||||
posthogApiHost = callAnalyticsCredentialsProvider.posthogApiHost.takeIf { isAnalyticsEnabled },
|
||||
posthogApiKey = callAnalyticsCredentialsProvider.posthogApiKey.takeIf { isAnalyticsEnabled },
|
||||
|
|
@ -51,13 +46,25 @@ class DefaultCallWidgetSettingsProvider(
|
|||
sentryDsn = callAnalyticsCredentialsProvider.sentryDsn.takeIf { isAnalyticsEnabled },
|
||||
sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG",
|
||||
parentUrl = null,
|
||||
// For backwards compatibility, it'll be ignored in recent versions of Element Call
|
||||
hideHeader = true,
|
||||
controlledMediaDevices = true,
|
||||
header = HeaderStyle.APP_BAR,
|
||||
sendNotificationType = if (direct) NotificationType.RING else NotificationType.NOTIFICATION,
|
||||
)
|
||||
val rustWidgetSettings = newVirtualElementCallWidget(options)
|
||||
val config = VirtualElementCallWidgetConfig(
|
||||
// TODO remove this once we have the next EC version
|
||||
preload = false,
|
||||
// TODO remove this once we have the next EC version
|
||||
skipLobby = null,
|
||||
intent = when {
|
||||
direct && hasActiveCall -> CallIntent.JOIN_EXISTING_DM
|
||||
hasActiveCall -> CallIntent.JOIN_EXISTING
|
||||
direct -> CallIntent.START_CALL_DM
|
||||
else -> CallIntent.START_CALL
|
||||
}.also {
|
||||
Timber.d("Starting/joining call with intent: $it")
|
||||
}
|
||||
)
|
||||
val rustWidgetSettings = newVirtualElementCallWidget(
|
||||
props = properties,
|
||||
config = config,
|
||||
)
|
||||
return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ package io.element.android.libraries.matrix.impl
|
|||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
|
||||
class FakeClientBuilderProvider : ClientBuilderProvider {
|
||||
class FakeClientBuilderProvider(
|
||||
private val provideResult: () -> ClientBuilder = { FakeFfiClientBuilder() }
|
||||
) : ClientBuilderProvider {
|
||||
override fun provide(): ClientBuilder {
|
||||
return FakeFfiClientBuilder()
|
||||
return provideResult()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ fun TestScope.createRustMatrixClientFactory(
|
|||
baseDirectory: File = File("/base"),
|
||||
cacheDirectory: File = File("/cache"),
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
) = RustMatrixClientFactory(
|
||||
baseDirectory = baseDirectory,
|
||||
cacheDirectory = cacheDirectory,
|
||||
|
|
@ -52,5 +53,5 @@ fun TestScope.createRustMatrixClientFactory(
|
|||
analyticsService = FakeAnalyticsService(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
|
||||
clientBuilderProvider = FakeClientBuilderProvider(),
|
||||
clientBuilderProvider = clientBuilderProvider,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,14 +8,17 @@
|
|||
package io.element.android.libraries.matrix.impl.auth
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -24,18 +27,28 @@ import java.io.File
|
|||
|
||||
class RustMatrixAuthenticationServiceTest {
|
||||
@Test
|
||||
fun `getLatestSessionId should return the value from the store`() = runTest {
|
||||
val sessionStore = InMemorySessionStore()
|
||||
fun `setHomeserver is successful`() = runTest {
|
||||
val sut = createRustMatrixAuthenticationService(
|
||||
sessionStore = sessionStore,
|
||||
clientBuilderProvider = FakeClientBuilderProvider(
|
||||
provideResult = {
|
||||
FakeFfiClientBuilder(
|
||||
buildResult = {
|
||||
FakeFfiClient(
|
||||
homeserverLoginDetailsResult = {
|
||||
FakeFfiHomeserverLoginDetails()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
assertThat(sut.getLatestSessionId()).isNull()
|
||||
sessionStore.storeData(aSessionData(sessionId = "@alice:server.org"))
|
||||
assertThat(sut.getLatestSessionId()).isEqualTo(SessionId("@alice:server.org"))
|
||||
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||
}
|
||||
|
||||
private fun TestScope.createRustMatrixAuthenticationService(
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
): RustMatrixAuthenticationService {
|
||||
val baseDirectory = File("/base")
|
||||
val cacheDirectory = File("/cache")
|
||||
|
|
@ -43,6 +56,7 @@ class RustMatrixAuthenticationServiceTest {
|
|||
baseDirectory = baseDirectory,
|
||||
cacheDirectory = cacheDirectory,
|
||||
sessionStore = sessionStore,
|
||||
clientBuilderProvider = clientBuilderProvider,
|
||||
)
|
||||
return RustMatrixAuthenticationService(
|
||||
sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ fun aRustRoomMember(
|
|||
membership = membership,
|
||||
isNameAmbiguous = isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = powerLevel,
|
||||
isIgnored = isIgnored,
|
||||
suggestedRoleForPowerLevel = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
|
|
|
|||
|
|
@ -43,4 +43,5 @@ fun aRustSpaceRoom(
|
|||
childrenCount = childrenCount,
|
||||
state = state,
|
||||
heroes = heroes,
|
||||
via = emptyList()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import io.element.android.tests.testutils.simulateLongTask
|
|||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import org.matrix.rustcomponents.sdk.Encryption
|
||||
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
|
||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||
|
|
@ -41,6 +42,7 @@ class FakeFfiClient(
|
|||
private val session: Session = aRustSession(),
|
||||
private val clearCachesResult: () -> Unit = { lambdaError() },
|
||||
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
|
||||
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
|
||||
private val closeResult: () -> Unit = {},
|
||||
) : Client(NoPointer) {
|
||||
override fun userId(): String = userId
|
||||
|
|
@ -71,6 +73,7 @@ class FakeFfiClient(
|
|||
override suspend fun ignoredUsers(): List<String> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun subscribeToIgnoredUsers(listener: IgnoredUsersListener): TaskHandle {
|
||||
return FakeFfiTaskHandle()
|
||||
}
|
||||
|
|
@ -78,5 +81,10 @@ class FakeFfiClient(
|
|||
override suspend fun getProfile(userId: String): UserProfile {
|
||||
return UserProfile(userId = userId, displayName = null, avatarUrl = null)
|
||||
}
|
||||
|
||||
override suspend fun homeserverLoginDetails(): HomeserverLoginDetails {
|
||||
return homeserverLoginDetailsResult()
|
||||
}
|
||||
|
||||
override fun close() = closeResult()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ import uniffi.matrix_sdk.BackupDownloadStrategy
|
|||
import uniffi.matrix_sdk_crypto.CollectStrategy
|
||||
import uniffi.matrix_sdk_crypto.DecryptionSettings
|
||||
|
||||
class FakeFfiClientBuilder : ClientBuilder(NoPointer) {
|
||||
class FakeFfiClientBuilder(
|
||||
val buildResult: () -> Client = { FakeFfiClient(withUtdHook = {}) }
|
||||
) : ClientBuilder(NoPointer) {
|
||||
override fun addRootCertificates(certificates: List<ByteArray>) = this
|
||||
override fun autoEnableBackups(autoEnableBackups: Boolean) = this
|
||||
override fun autoEnableCrossSigning(autoEnableCrossSigning: Boolean) = this
|
||||
|
|
@ -41,7 +43,5 @@ class FakeFfiClientBuilder : ClientBuilder(NoPointer) {
|
|||
override fun enableShareHistoryOnInvite(enableShareHistoryOnInvite: Boolean): ClientBuilder = this
|
||||
override fun threadsEnabled(enabled: Boolean, threadSubscriptions: Boolean): ClientBuilder = this
|
||||
|
||||
override suspend fun build(): Client {
|
||||
return FakeFfiClient(withUtdHook = {})
|
||||
}
|
||||
override suspend fun build() = buildResult()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.SpaceListUpdate
|
||||
import org.matrix.rustcomponents.sdk.SpaceRoom
|
||||
import org.matrix.rustcomponents.sdk.SpaceRoomList
|
||||
import org.matrix.rustcomponents.sdk.SpaceRoomListEntriesListener
|
||||
import org.matrix.rustcomponents.sdk.SpaceRoomListPaginationStateListener
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
|
||||
|
||||
class FakeFfiSpaceRoomList(
|
||||
private val paginateResult: () -> Unit = { lambdaError() },
|
||||
private val paginationStateResult: () -> SpaceRoomListPaginationState = { lambdaError() },
|
||||
private val roomsResult: () -> List<SpaceRoom> = { lambdaError() },
|
||||
) : SpaceRoomList(NoPointer) {
|
||||
private var spaceRoomListPaginationStateListener: SpaceRoomListPaginationStateListener? = null
|
||||
private var spaceRoomListEntriesListener: SpaceRoomListEntriesListener? = null
|
||||
|
||||
override suspend fun paginate() = simulateLongTask {
|
||||
paginateResult()
|
||||
}
|
||||
|
||||
override fun paginationState(): SpaceRoomListPaginationState {
|
||||
return paginationStateResult()
|
||||
}
|
||||
|
||||
override fun rooms(): List<SpaceRoom> {
|
||||
return roomsResult()
|
||||
}
|
||||
|
||||
override fun subscribeToPaginationStateUpdates(listener: SpaceRoomListPaginationStateListener): TaskHandle {
|
||||
spaceRoomListPaginationStateListener = listener
|
||||
return FakeFfiTaskHandle()
|
||||
}
|
||||
|
||||
fun triggerPaginationStateUpdate(state: SpaceRoomListPaginationState) {
|
||||
spaceRoomListPaginationStateListener?.onUpdate(state)
|
||||
}
|
||||
|
||||
override fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle {
|
||||
spaceRoomListEntriesListener = listener
|
||||
return FakeFfiTaskHandle()
|
||||
}
|
||||
|
||||
fun triggerRoomListUpdate(rooms: List<SpaceListUpdate>) {
|
||||
spaceRoomListEntriesListener?.onUpdate(rooms)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ class MessageEventTypeKtTest {
|
|||
assertThat(MessageLikeEventType.CALL_INVITE.map()).isEqualTo(MessageEventType.CALL_INVITE)
|
||||
assertThat(MessageLikeEventType.CALL_HANGUP.map()).isEqualTo(MessageEventType.CALL_HANGUP)
|
||||
assertThat(MessageLikeEventType.CALL_CANDIDATES.map()).isEqualTo(MessageEventType.CALL_CANDIDATES)
|
||||
assertThat(MessageLikeEventType.CALL_NOTIFY.map()).isEqualTo(MessageEventType.CALL_NOTIFY)
|
||||
assertThat(MessageLikeEventType.RTC_NOTIFICATION.map()).isEqualTo(MessageEventType.RTC_NOTIFICATION)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_READY)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_START)
|
||||
assertThat(MessageLikeEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_CANCEL)
|
||||
|
|
@ -46,7 +46,7 @@ class MessageEventTypeKtTest {
|
|||
assertThat(MessageEventType.CALL_INVITE.map()).isEqualTo(MessageLikeEventType.CALL_INVITE)
|
||||
assertThat(MessageEventType.CALL_HANGUP.map()).isEqualTo(MessageLikeEventType.CALL_HANGUP)
|
||||
assertThat(MessageEventType.CALL_CANDIDATES.map()).isEqualTo(MessageLikeEventType.CALL_CANDIDATES)
|
||||
assertThat(MessageEventType.CALL_NOTIFY.map()).isEqualTo(MessageLikeEventType.CALL_NOTIFY)
|
||||
assertThat(MessageEventType.RTC_NOTIFICATION.map()).isEqualTo(MessageLikeEventType.RTC_NOTIFICATION)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_READY)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_START)
|
||||
assertThat(MessageEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_CANCEL)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.matrix.impl.spaces
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSpaceRoom
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSpaceRoomList
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.SpaceListUpdate
|
||||
import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
|
||||
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
|
||||
|
||||
class RustSpaceRoomListTest {
|
||||
@Test
|
||||
fun `paginationStatusFlow emits values`() = runTest {
|
||||
val innerSpaceRoomList = FakeFfiSpaceRoomList(
|
||||
paginationStateResult = { SpaceRoomListPaginationState.Idle(false) }
|
||||
)
|
||||
val sut = createRustSpaceRoomList(
|
||||
innerSpaceRoomList = innerSpaceRoomList,
|
||||
)
|
||||
sut.paginationStatusFlow.test {
|
||||
// First value is the initial one
|
||||
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
|
||||
// First value after the subscription occurs
|
||||
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true))
|
||||
innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Loading)
|
||||
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Loading)
|
||||
innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Idle(true))
|
||||
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = false))
|
||||
innerSpaceRoomList.triggerPaginationStateUpdate(SpaceRoomListPaginationState.Idle(false))
|
||||
assertThat(awaitItem()).isEqualTo(SpaceRoomList.PaginationStatus.Idle(hasMoreToLoad = true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spaceRoomsFlow emits values`() = runTest {
|
||||
val innerSpaceRoomList = FakeFfiSpaceRoomList(
|
||||
paginationStateResult = { SpaceRoomListPaginationState.Idle(false) }
|
||||
)
|
||||
val sut = createRustSpaceRoomList(
|
||||
innerSpaceRoomList = innerSpaceRoomList,
|
||||
)
|
||||
sut.spaceRoomsFlow.test {
|
||||
// Give time for the subscription to be set
|
||||
runCurrent()
|
||||
innerSpaceRoomList.triggerRoomListUpdate(
|
||||
listOf(
|
||||
SpaceListUpdate.PushBack(aRustSpaceRoom(roomId = A_ROOM_ID_2))
|
||||
)
|
||||
)
|
||||
val rooms = awaitItem()
|
||||
assertThat(rooms).hasSize(1)
|
||||
assertThat(rooms[0].roomId).isEqualTo(A_ROOM_ID_2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `paginate invokes paginate on the inner class`() = runTest {
|
||||
val paginateResult = lambdaRecorder<Unit> { }
|
||||
val innerSpaceRoomList = FakeFfiSpaceRoomList(
|
||||
paginateResult = paginateResult,
|
||||
)
|
||||
val sut = createRustSpaceRoomList(
|
||||
innerSpaceRoomList = innerSpaceRoomList,
|
||||
)
|
||||
sut.paginate()
|
||||
paginateResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `currentSpaceFlow reads value from the SpaceRoomCache`() = runTest {
|
||||
val spaceRoomCache = SpaceRoomCache()
|
||||
val sut = createRustSpaceRoomList(
|
||||
spaceRoomCache = spaceRoomCache,
|
||||
)
|
||||
sut.currentSpaceFlow().test {
|
||||
assertThat(awaitItem()).isNull()
|
||||
val spaceRoom = aSpaceRoom(roomId = A_ROOM_ID)
|
||||
spaceRoomCache.update(listOf(spaceRoom))
|
||||
assertThat(awaitItem()).isEqualTo(spaceRoom)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createRustSpaceRoomList(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
innerSpaceRoomList: InnerSpaceRoomList = FakeFfiSpaceRoomList(),
|
||||
innerProvider: suspend () -> InnerSpaceRoomList = { innerSpaceRoomList },
|
||||
spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(),
|
||||
spaceRoomCache: SpaceRoomCache = SpaceRoomCache(),
|
||||
): RustSpaceRoomList {
|
||||
return RustSpaceRoomList(
|
||||
roomId = roomId,
|
||||
innerProvider = innerProvider,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
spaceRoomMapper = spaceRoomMapper,
|
||||
spaceRoomCache = spaceRoomCache,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +90,6 @@ class FakeMatrixClient(
|
|||
private val canDeactivateAccountResult: () -> Boolean = { lambdaError() },
|
||||
private val deactivateAccountResult: (String, Boolean) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||
private val currentSlidingSyncVersionLambda: () -> Result<SlidingSyncVersion> = { lambdaError() },
|
||||
private val availableSlidingSyncVersionsLambda: () -> Result<List<SlidingSyncVersion>> = { lambdaError() },
|
||||
private val ignoreUserResult: (UserId) -> Result<Unit> = { lambdaError() },
|
||||
private var unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
|
||||
private val canReportRoomLambda: () -> Boolean = { false },
|
||||
|
|
@ -339,10 +338,6 @@ class FakeMatrixClient(
|
|||
return currentSlidingSyncVersionLambda()
|
||||
}
|
||||
|
||||
override suspend fun availableSlidingSyncVersions(): Result<List<SlidingSyncVersion>> {
|
||||
return availableSlidingSyncVersionsLambda()
|
||||
}
|
||||
|
||||
override suspend fun canReportRoom(): Boolean {
|
||||
return canReportRoomLambda()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const val A_USER_NAME_2 = "Bob"
|
|||
const val A_PASSWORD = "password"
|
||||
const val A_PASSPHRASE = "passphrase"
|
||||
const val A_SECRET = "secret"
|
||||
const val AN_APPLICATION_NAME = "AppName"
|
||||
|
||||
val A_USER_ID = UserId("@alice:server.org")
|
||||
val A_USER_ID_2 = UserId("@bob:server.org")
|
||||
|
|
|
|||
|
|
@ -19,14 +19,11 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
val A_OIDC_DATA = OidcDetails(url = "a-url")
|
||||
|
||||
|
|
@ -44,14 +41,6 @@ class FakeMatrixAuthenticationService(
|
|||
private var matrixClient: MatrixClient? = null
|
||||
private var onAuthenticationListener: ((MatrixClient) -> Unit)? = null
|
||||
|
||||
var getLatestSessionIdLambda: (() -> SessionId?) = { null }
|
||||
|
||||
override fun loggedInStateFlow(): Flow<LoggedInState> {
|
||||
return flowOf(LoggedInState.NotLoggedIn)
|
||||
}
|
||||
|
||||
override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda()
|
||||
|
||||
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
|
||||
matrixClientResult?.let {
|
||||
return it.invoke(sessionId)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
|
@ -77,6 +79,12 @@ class FakeBaseRoom(
|
|||
_roomInfoFlow.tryEmit(roomInfo)
|
||||
}
|
||||
|
||||
private val declineCallFlowMap: MutableMap<EventId, MutableSharedFlow<UserId>> = mutableMapOf()
|
||||
|
||||
suspend fun givenDecliner(userId: UserId, forNotificationEventId: EventId) {
|
||||
declineCallFlowMap[forNotificationEventId]?.emit(userId)
|
||||
}
|
||||
|
||||
override val membersStateFlow: MutableStateFlow<RoomMembersState> = MutableStateFlow(RoomMembersState.Unknown)
|
||||
|
||||
override suspend fun updateMembers() = updateMembersResult()
|
||||
|
|
@ -222,6 +230,15 @@ class FakeBaseRoom(
|
|||
|
||||
override suspend fun reportRoom(reason: String?) = reportRoomResult(reason)
|
||||
|
||||
override suspend fun declineCall(notificationEventId: EventId): Result<Unit> {
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow<UserId> {
|
||||
val flow = declineCallFlowMap.getOrPut(notificationEventId, { MutableSharedFlow() })
|
||||
return flow
|
||||
}
|
||||
|
||||
override fun predecessorRoom(): PredecessorRoom? = predecessorRoomResult()
|
||||
|
||||
fun givenUpdateMembersResult(result: () -> Unit) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ fun aRoomMember(
|
|||
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous: Boolean = false,
|
||||
powerLevel: Long = 0L,
|
||||
normalizedPowerLevel: Long = 0L,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMember.Role = RoomMember.Role.User,
|
||||
membershipChangeReason: String? = null,
|
||||
|
|
@ -30,7 +29,6 @@ fun aRoomMember(
|
|||
membership = membership,
|
||||
isNameAmbiguous = isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = normalizedPowerLevel,
|
||||
isIgnored = isIgnored,
|
||||
role = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ class FakeSpaceRoomList(
|
|||
initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading,
|
||||
private val paginateResult: () -> Result<Unit> = { lambdaError() },
|
||||
) : SpaceRoomList {
|
||||
private val _currentSpaceFlow: MutableStateFlow<Optional<SpaceRoom>> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue))
|
||||
override fun currentSpaceFlow() = _currentSpaceFlow.asStateFlow()
|
||||
private val currentSpaceMutableStateFlow: MutableStateFlow<Optional<SpaceRoom>> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue))
|
||||
override fun currentSpaceFlow(): StateFlow<Optional<SpaceRoom>> = currentSpaceMutableStateFlow.asStateFlow()
|
||||
|
||||
fun emitCurrentSpace(value: SpaceRoom?) {
|
||||
_currentSpaceFlow.value = Optional.ofNullable(value)
|
||||
currentSpaceMutableStateFlow.value = Optional.ofNullable(value)
|
||||
}
|
||||
|
||||
private val _spaceRoomsFlow: MutableStateFlow<List<SpaceRoom>> = MutableStateFlow(initialSpaceRoomsValue)
|
||||
|
|
|
|||
|
|
@ -304,9 +304,9 @@ class FakeTimeline(
|
|||
)
|
||||
}
|
||||
|
||||
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Unit> = { _, _ -> lambdaError() }
|
||||
var toggleReactionLambda: (emoji: String, eventOrTransactionId: EventOrTransactionId) -> Result<Boolean> = { _, _ -> lambdaError() }
|
||||
|
||||
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Unit> = simulateLongTask {
|
||||
override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result<Boolean> = simulateLongTask {
|
||||
toggleReactionLambda(
|
||||
emoji,
|
||||
eventOrTransactionId,
|
||||
|
|
|
|||
|
|
@ -11,12 +11,18 @@ import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
|||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||
|
||||
class FakeCallWidgetSettingsProvider(
|
||||
private val provideFn: (String, String, Boolean, Boolean) -> MatrixWidgetSettings = { _, _, _, _ -> MatrixWidgetSettings("id", true, "url") }
|
||||
private val provideFn: (String, String, Boolean, Boolean, Boolean) -> MatrixWidgetSettings = { _, _, _, _, _ -> MatrixWidgetSettings("id", true, "url") }
|
||||
) : CallWidgetSettingsProvider {
|
||||
val providedBaseUrls = mutableListOf<String>()
|
||||
|
||||
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean, direct: Boolean): MatrixWidgetSettings {
|
||||
override suspend fun provide(
|
||||
baseUrl: String,
|
||||
widgetId: String,
|
||||
encrypted: Boolean,
|
||||
direct: Boolean,
|
||||
hasActiveCall: Boolean
|
||||
): MatrixWidgetSettings {
|
||||
providedBaseUrls += baseUrl
|
||||
return provideFn(baseUrl, widgetId, encrypted, direct)
|
||||
return provideFn(baseUrl, widgetId, encrypted, direct, hasActiveCall)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -37,17 +38,8 @@ dependencies {
|
|||
implementation(libs.coil.network.okhttp)
|
||||
implementation(libs.jsoup)
|
||||
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -18,9 +20,6 @@ android {
|
|||
implementation(projects.libraries.di)
|
||||
implementation(libs.inject)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -26,12 +27,7 @@ dependencies {
|
|||
|
||||
implementation(libs.coroutines.core)
|
||||
|
||||
testImplementation(projects.tests.testutils)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.audio.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.coroutines.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -27,12 +28,8 @@ dependencies {
|
|||
implementation(libs.inject)
|
||||
implementation(libs.coroutines.core)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.robolectric)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -39,10 +40,6 @@ dependencies {
|
|||
implementation(libs.coroutines.core)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -52,21 +53,12 @@ dependencies {
|
|||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.audio.test)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.mediaviewer.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import dev.zacsweers.metro.ContributesBinding
|
|||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryRootNode
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryFlowNode
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
|
|
@ -30,7 +30,7 @@ class DefaultMediaGalleryEntryPoint : MediaGalleryEntryPoint {
|
|||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<MediaGalleryRootNode>(buildContext, plugins)
|
||||
return parentNode.createNode<MediaGalleryFlowNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||
|
||||
interface FocusedTimelineMediaGalleryDataSourceFactory {
|
||||
fun interface FocusedTimelineMediaGalleryDataSourceFactory {
|
||||
fun createFor(
|
||||
eventId: EventId,
|
||||
mediaItem: MediaItem.Event,
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ import kotlinx.parcelize.Parcelize
|
|||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@Inject
|
||||
class MediaGalleryRootNode(
|
||||
class MediaGalleryFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val mediaViewerEntryPoint: MediaViewerEntryPoint
|
||||
) : BaseFlowNode<MediaGalleryRootNode.NavTarget>(
|
||||
) : BaseFlowNode<MediaGalleryFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
|
|
@ -87,11 +87,11 @@ class MediaGalleryRootNode(
|
|||
NavTarget.Root -> {
|
||||
val callback = object : MediaGalleryNode.Callback {
|
||||
override fun onBackClick() {
|
||||
this@MediaGalleryRootNode.onBackClick()
|
||||
this@MediaGalleryFlowNode.onBackClick()
|
||||
}
|
||||
|
||||
override fun onViewInTimeline(eventId: EventId) {
|
||||
this@MediaGalleryRootNode.onViewInTimeline(eventId)
|
||||
this@MediaGalleryFlowNode.onViewInTimeline(eventId)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MediaItem.Event) {
|
||||
|
|
@ -122,7 +122,7 @@ class MediaGalleryRootNode(
|
|||
}
|
||||
|
||||
override fun onViewInTimeline(eventId: EventId) {
|
||||
this@MediaGalleryRootNode.onViewInTimeline(eventId)
|
||||
this@MediaGalleryFlowNode.onViewInTimeline(eventId)
|
||||
}
|
||||
}
|
||||
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
|
||||
|
|
@ -10,12 +10,14 @@ package io.element.android.libraries.mediaviewer.impl.local.video
|
|||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -33,6 +35,7 @@ import androidx.media3.common.MediaItem
|
|||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.STATE_READY
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerView
|
||||
|
|
@ -55,6 +58,7 @@ import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay
|
|||
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
|
||||
import kotlinx.coroutines.delay
|
||||
import me.saket.telephoto.zoomable.zoomable
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
|
|
@ -165,35 +169,6 @@ private fun ExoPlayerMediaVideoView(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(exoPlayer.isPlaying) {
|
||||
if (exoPlayer.isPlaying) {
|
||||
while (true) {
|
||||
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||
progressInMillis = exoPlayer.currentPosition,
|
||||
)
|
||||
delay(200)
|
||||
}
|
||||
} else {
|
||||
// Ensure we render the final state
|
||||
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||
progressInMillis = exoPlayer.currentPosition,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var needsAutoPlay by remember { mutableStateOf(autoplay) }
|
||||
|
||||
LaunchedEffect(needsAutoPlay, isDisplayed, mediaPlayerControllerState.isReady) {
|
||||
val isReadyAndNotPlaying = mediaPlayerControllerState.isReady && !mediaPlayerControllerState.isPlaying
|
||||
if (needsAutoPlay && isDisplayed && isReadyAndNotPlaying) {
|
||||
// When displayed, start autoplaying
|
||||
exoPlayer.play()
|
||||
needsAutoPlay = false
|
||||
} else if (!isDisplayed && mediaPlayerControllerState.isPlaying) {
|
||||
// If not displayed, make sure to pause the video
|
||||
exoPlayer.pause()
|
||||
}
|
||||
}
|
||||
if (localMedia?.uri != null) {
|
||||
LaunchedEffect(localMedia.uri) {
|
||||
val mediaItem = MediaItem.fromUri(localMedia.uri)
|
||||
|
|
@ -263,16 +238,72 @@ private fun ExoPlayerMediaVideoView(
|
|||
)
|
||||
}
|
||||
|
||||
OnLifecycleEvent { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_CREATE -> exoPlayer.addListener(playerListener)
|
||||
Lifecycle.Event.ON_RESUME -> exoPlayer.prepare()
|
||||
Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
|
||||
Lifecycle.Event.ON_DESTROY -> {
|
||||
exoPlayer.release()
|
||||
exoPlayer.removeListener(playerListener)
|
||||
LaunchedEffect(exoPlayer.isPlaying) {
|
||||
if (exoPlayer.isPlaying) {
|
||||
while (true) {
|
||||
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||
progressInMillis = exoPlayer.currentPosition,
|
||||
)
|
||||
delay(200)
|
||||
}
|
||||
else -> Unit
|
||||
} else {
|
||||
// Ensure we render the final state
|
||||
mediaPlayerControllerState = mediaPlayerControllerState.copy(
|
||||
progressInMillis = exoPlayer.currentPosition,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExoPlayerLifecycleHelper(
|
||||
exoPlayer = exoPlayer,
|
||||
autoplay = autoplay,
|
||||
isDisplayed = isDisplayed,
|
||||
playerListener = playerListener,
|
||||
mediaPlayerControllerState = mediaPlayerControllerState,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
private fun ExoPlayerLifecycleHelper(
|
||||
exoPlayer: ExoPlayer,
|
||||
autoplay: Boolean,
|
||||
isDisplayed: Boolean,
|
||||
playerListener: Player.Listener,
|
||||
mediaPlayerControllerState: MediaPlayerControllerState,
|
||||
) {
|
||||
// Prepare and release the exoPlayer with the composable lifecycle
|
||||
DisposableEffect(Unit) {
|
||||
Timber.d("ExoPlayerMediaVideoView DisposableEffect: initializing exoPlayer")
|
||||
exoPlayer.addListener(playerListener)
|
||||
exoPlayer.prepare()
|
||||
|
||||
onDispose {
|
||||
Timber.d("Disposing exoplayer")
|
||||
if (!exoPlayer.isReleased) {
|
||||
exoPlayer.removeListener(playerListener)
|
||||
exoPlayer.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var needsAutoPlay by remember { mutableStateOf(autoplay) }
|
||||
LaunchedEffect(needsAutoPlay, isDisplayed, mediaPlayerControllerState.isReady) {
|
||||
val isReadyAndNotPlaying = mediaPlayerControllerState.isReady && !mediaPlayerControllerState.isPlaying
|
||||
if (needsAutoPlay && isDisplayed && isReadyAndNotPlaying) {
|
||||
// When displayed, start autoplaying
|
||||
exoPlayer.play()
|
||||
needsAutoPlay = false
|
||||
} else if (!isDisplayed && mediaPlayerControllerState.isPlaying) {
|
||||
// If not displayed, make sure to pause the video
|
||||
exoPlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
// Pause playback when lifecycle is paused
|
||||
OnLifecycleEvent { _, event ->
|
||||
if (event == Lifecycle.Event.ON_PAUSE && exoPlayer.isPlaying) {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class MediaViewerPresenter(
|
|||
private val localMediaActions: LocalMediaActions,
|
||||
) : Presenter<MediaViewerState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun interface Factory {
|
||||
fun create(
|
||||
inputs: MediaViewerEntryPoint.Params,
|
||||
navigator: MediaViewerNavigator,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryFlowNode
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultMediaGalleryEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultMediaGalleryEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
MediaGalleryFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
mediaViewerEntryPoint = object : MediaViewerEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext) = lambdaError()
|
||||
}
|
||||
)
|
||||
}
|
||||
val callback = object : MediaGalleryEntryPoint.Callback {
|
||||
override fun onBackClick() = lambdaError()
|
||||
override fun onViewInTimeline(eventId: EventId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(MediaGalleryFlowNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
|
||||
import io.element.android.libraries.mediaplayer.test.FakeAudioFocus
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.impl.datasource.createTimelineMediaGalleryDataSource
|
||||
import io.element.android.libraries.mediaviewer.impl.viewer.MediaViewerNode
|
||||
import io.element.android.libraries.mediaviewer.impl.viewer.PagerKeysHandler
|
||||
import io.element.android.libraries.mediaviewer.impl.viewer.createMediaViewerEntryPointParams
|
||||
import io.element.android.libraries.mediaviewer.impl.viewer.createMediaViewerPresenter
|
||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultMediaViewerEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() = runTest {
|
||||
val entryPoint = DefaultMediaViewerEntryPoint()
|
||||
val mockMediaUri: Uri = mockk("localMediaUri")
|
||||
val localMediaFactory = FakeLocalMediaFactory(mockMediaUri)
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
MediaViewerNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenterFactory = { _, _, _ ->
|
||||
createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
)
|
||||
},
|
||||
timelineMediaGalleryDataSource = createTimelineMediaGalleryDataSource(),
|
||||
focusedTimelineMediaGalleryDataSourceFactory = { _, _, _ ->
|
||||
lambdaError()
|
||||
},
|
||||
mediaLoader = FakeMatrixMediaLoader(),
|
||||
localMediaFactory = FakeLocalMediaFactory(mockMediaUri),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
systemClock = FakeSystemClock(),
|
||||
pagerKeysHandler = PagerKeysHandler(),
|
||||
textFileViewer = { _, _ -> lambdaError() },
|
||||
audioFocus = FakeAudioFocus(),
|
||||
)
|
||||
}
|
||||
val callback = object : MediaViewerEntryPoint.Callback {
|
||||
override fun onDone() = lambdaError()
|
||||
override fun onViewInTimeline(eventId: EventId) = lambdaError()
|
||||
}
|
||||
val params = createMediaViewerEntryPointParams()
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.params(params)
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(MediaViewerNode::class.java)
|
||||
assertThat(result.plugins).contains(params)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test node builder avatar`() = runTest {
|
||||
val entryPoint = DefaultMediaViewerEntryPoint()
|
||||
val mockMediaUri: Uri = mockk("localMediaUri")
|
||||
val localMediaFactory = FakeLocalMediaFactory(mockMediaUri)
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
MediaViewerNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenterFactory = { _, _, _ ->
|
||||
createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
)
|
||||
},
|
||||
timelineMediaGalleryDataSource = createTimelineMediaGalleryDataSource(),
|
||||
focusedTimelineMediaGalleryDataSourceFactory = { _, _, _ ->
|
||||
lambdaError()
|
||||
},
|
||||
mediaLoader = FakeMatrixMediaLoader(),
|
||||
localMediaFactory = FakeLocalMediaFactory(mockMediaUri),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
systemClock = FakeSystemClock(),
|
||||
pagerKeysHandler = PagerKeysHandler(),
|
||||
textFileViewer = { _, _ -> lambdaError() },
|
||||
audioFocus = FakeAudioFocus(),
|
||||
)
|
||||
}
|
||||
val callback = object : MediaViewerEntryPoint.Callback {
|
||||
override fun onDone() = lambdaError()
|
||||
override fun onViewInTimeline(eventId: EventId) = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.avatar(
|
||||
filename = "fn",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(MediaViewerNode::class.java)
|
||||
assertThat(result.plugins).contains(
|
||||
MediaViewerEntryPoint.Params(
|
||||
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
||||
eventId = null,
|
||||
mediaInfo = MediaInfo(
|
||||
filename = "fn",
|
||||
fileSize = null,
|
||||
caption = null,
|
||||
mimeType = MimeTypes.Images,
|
||||
formattedFileSize = "",
|
||||
fileExtension = "",
|
||||
senderId = UserId("@dummy:server.org"),
|
||||
senderName = null,
|
||||
senderAvatar = null,
|
||||
dateSent = null,
|
||||
dateSentFull = null,
|
||||
waveform = null,
|
||||
duration = null,
|
||||
),
|
||||
mediaSource = MediaSource(url = "avatarUrl"),
|
||||
thumbnailSource = null,
|
||||
canShowInfo = false,
|
||||
)
|
||||
)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -255,19 +255,19 @@ class TimelineMediaGalleryDataSourceTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createTimelineMediaGalleryDataSource(
|
||||
room: JoinedRoom = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline(),
|
||||
),
|
||||
): TimelineMediaGalleryDataSource {
|
||||
return TimelineMediaGalleryDataSource(
|
||||
room = room,
|
||||
mediaTimeline = LiveMediaTimeline(room),
|
||||
timelineMediaItemsFactory = createTimelineMediaItemsFactory(),
|
||||
mediaItemsPostProcessor = MediaItemsPostProcessor(),
|
||||
)
|
||||
}
|
||||
internal fun TestScope.createTimelineMediaGalleryDataSource(
|
||||
room: JoinedRoom = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline(),
|
||||
),
|
||||
): TimelineMediaGalleryDataSource {
|
||||
return TimelineMediaGalleryDataSource(
|
||||
room = room,
|
||||
mediaTimeline = LiveMediaTimeline(room),
|
||||
timelineMediaItemsFactory = createTimelineMediaItemsFactory(),
|
||||
mediaItemsPostProcessor = MediaItemsPostProcessor(),
|
||||
)
|
||||
}
|
||||
|
||||
fun TestScope.createTimelineMediaItemsFactory() = TimelineMediaItemsFactory(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ package io.element.android.libraries.mediaviewer.impl.gallery
|
|||
import android.net.Uri
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
|
@ -29,6 +31,7 @@ import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetSta
|
|||
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage
|
||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
|
||||
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
|
@ -147,8 +150,8 @@ class MediaGalleryPresenterTest {
|
|||
val presenter = createMediaGalleryPresenter(
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
sessionId = A_USER_ID,
|
||||
initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
|
||||
sessionId = A_USER_ID,
|
||||
initialRoomInfo = aRoomInfo(name = A_ROOM_NAME),
|
||||
canRedactOtherResult = { Result.success(canDeleteOther) },
|
||||
),
|
||||
createTimelineResult = { Result.success(FakeTimeline()) }
|
||||
|
|
@ -223,23 +226,122 @@ class MediaGalleryPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - share item`() = runTest {
|
||||
fun `present - share item - item not found`() = runTest {
|
||||
val presenter = createMediaGalleryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID))
|
||||
}
|
||||
// TODO Add more test on this part
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save on disk`() = runTest {
|
||||
fun `present - share item - item found`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
aGroupedMediaItems(
|
||||
imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)),
|
||||
fileItems = emptyList(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.snackbarMessage).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - share item - item found - download error`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
aGroupedMediaItems(
|
||||
imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)),
|
||||
fileItems = emptyList(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
matrixMediaLoader = FakeMatrixMediaLoader().apply { shouldFail = true },
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.Share(AN_EVENT_ID))
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.snackbarMessage).isInstanceOf(SnackbarMessage::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save on disk - item not found`() = runTest {
|
||||
val presenter = createMediaGalleryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID))
|
||||
}
|
||||
// TODO Add more test on this part
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save on disk - item found`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
aGroupedMediaItems(
|
||||
imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)),
|
||||
fileItems = emptyList(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID))
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.snackbarMessage?.messageResId).isEqualTo(CommonStrings.common_file_saved_on_disk_android)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - save on disk - item found - download error`() = runTest {
|
||||
val mediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
)
|
||||
mediaGalleryDataSource.emitGroupedMediaItems(
|
||||
AsyncData.Success(
|
||||
aGroupedMediaItems(
|
||||
imageAndVideoItems = listOf(aMediaItemImage(eventId = AN_EVENT_ID)),
|
||||
fileItems = emptyList(),
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createMediaGalleryPresenter(
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
matrixMediaLoader = FakeMatrixMediaLoader().apply { shouldFail = true },
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(MediaGalleryEvents.SaveOnDisk(AN_EVENT_ID))
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.snackbarMessage).isInstanceOf(SnackbarMessage::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
|||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
||||
import io.element.android.libraries.mediaviewer.impl.R
|
||||
import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource
|
||||
import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource
|
||||
|
|
@ -78,12 +79,13 @@ class MediaViewerPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state null Event`() = runTest {
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
|
|
@ -97,13 +99,14 @@ class MediaViewerPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state cannot show info`() = runTest {
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
canShowInfo = false,
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
|
|
@ -117,13 +120,14 @@ class MediaViewerPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state Event`() = runTest {
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
eventId = AN_EVENT_ID,
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
|
|
@ -137,14 +141,15 @@ class MediaViewerPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state Event from other`() = runTest {
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
eventId = AN_EVENT_ID,
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
sessionId = A_SESSION_ID_2,
|
||||
canRedactOtherResult = { Result.success(false) },
|
||||
sessionId = A_SESSION_ID_2,
|
||||
canRedactOtherResult = { Result.success(false) },
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.listData).isEmpty()
|
||||
|
|
@ -161,6 +166,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage()
|
||||
|
|
@ -192,6 +198,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
|
|
@ -224,10 +231,13 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
room = FakeJoinedRoom(baseRoom = FakeBaseRoom(
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
))
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
mediaSourceUrl = aUrl,
|
||||
|
|
@ -266,6 +276,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
|
|
@ -298,6 +309,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
|
|
@ -330,6 +342,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
|
|
@ -362,6 +375,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
|
|
@ -394,6 +408,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
|
|
@ -441,6 +456,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
room = FakeJoinedRoom(
|
||||
liveTimeline = timeline,
|
||||
baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }),
|
||||
|
|
@ -498,6 +514,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
|
|
@ -549,6 +566,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mode = mode,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
|
|
@ -620,6 +638,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mode = mode,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
|
|
@ -674,6 +693,7 @@ class MediaViewerPresenterTest {
|
|||
startLambda = { },
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
presenter.test {
|
||||
|
|
@ -714,6 +734,7 @@ class MediaViewerPresenterTest {
|
|||
loadMoreLambda = loadMoreLambda,
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaGalleryDataSource = mediaGalleryDataSource,
|
||||
)
|
||||
val anImage = aMediaItemImage(
|
||||
|
|
@ -744,6 +765,7 @@ class MediaViewerPresenterTest {
|
|||
onViewInTimelineClickLambda = onViewInTimelineClickLambda,
|
||||
)
|
||||
val presenter = createMediaViewerPresenter(
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaViewerNavigator = navigator,
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }),
|
||||
|
|
@ -764,42 +786,53 @@ class MediaViewerPresenterTest {
|
|||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun TestScope.createMediaViewerPresenter(
|
||||
eventId: EventId? = null,
|
||||
mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
||||
matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
|
||||
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
|
||||
mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
),
|
||||
canShowInfo: Boolean = true,
|
||||
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
|
||||
room: JoinedRoom = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline(),
|
||||
),
|
||||
): MediaViewerPresenter {
|
||||
return MediaViewerPresenter(
|
||||
inputs = MediaViewerEntryPoint.Params(
|
||||
mode = mode,
|
||||
eventId = eventId,
|
||||
mediaInfo = TESTED_MEDIA_INFO,
|
||||
mediaSource = aMediaSource(),
|
||||
thumbnailSource = null,
|
||||
canShowInfo = canShowInfo,
|
||||
),
|
||||
navigator = mediaViewerNavigator,
|
||||
dataSource = MediaViewerDataSource(
|
||||
mode = mode,
|
||||
dispatcher = testCoroutineDispatchers().computation,
|
||||
galleryDataSource = mediaGalleryDataSource,
|
||||
mediaLoader = matrixMediaLoader,
|
||||
localMediaFactory = localMediaFactory,
|
||||
systemClock = FakeSystemClock(),
|
||||
pagerKeysHandler = PagerKeysHandler(),
|
||||
),
|
||||
room = room,
|
||||
localMediaActions = localMediaActions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TestScope.createMediaViewerPresenter(
|
||||
localMediaFactory: LocalMediaFactory,
|
||||
eventId: EventId? = null,
|
||||
mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
||||
matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
|
||||
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
|
||||
mediaGalleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(
|
||||
startLambda = { },
|
||||
),
|
||||
canShowInfo: Boolean = true,
|
||||
mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
|
||||
room: JoinedRoom = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline(),
|
||||
),
|
||||
): MediaViewerPresenter {
|
||||
return MediaViewerPresenter(
|
||||
inputs = createMediaViewerEntryPointParams(
|
||||
eventId = eventId,
|
||||
mode = mode,
|
||||
canShowInfo = canShowInfo,
|
||||
),
|
||||
navigator = mediaViewerNavigator,
|
||||
dataSource = MediaViewerDataSource(
|
||||
mode = mode,
|
||||
dispatcher = testCoroutineDispatchers().computation,
|
||||
galleryDataSource = mediaGalleryDataSource,
|
||||
mediaLoader = matrixMediaLoader,
|
||||
localMediaFactory = localMediaFactory,
|
||||
systemClock = FakeSystemClock(),
|
||||
pagerKeysHandler = PagerKeysHandler(),
|
||||
),
|
||||
room = room,
|
||||
localMediaActions = localMediaActions,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun createMediaViewerEntryPointParams(
|
||||
eventId: EventId? = null,
|
||||
mode: MediaViewerEntryPoint.MediaViewerMode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
||||
canShowInfo: Boolean = true,
|
||||
) = MediaViewerEntryPoint.Params(
|
||||
mode = mode,
|
||||
eventId = eventId,
|
||||
mediaInfo = TESTED_MEDIA_INFO,
|
||||
mediaSource = aMediaSource(),
|
||||
thumbnailSource = null,
|
||||
canShowInfo = canShowInfo,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ class SingleMediaGalleryDataSourceTest {
|
|||
assertThat(resultData.fileItems).isEmpty()
|
||||
}
|
||||
|
||||
private fun aMediaViewerEntryPointParams(
|
||||
internal fun aMediaViewerEntryPointParams(
|
||||
mediaInfo: MediaInfo,
|
||||
) = MediaViewerEntryPoint.Params(
|
||||
mode = MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -19,6 +21,5 @@ dependencies {
|
|||
implementation(projects.tests.testutils)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
|
|
@ -39,14 +40,7 @@ dependencies {
|
|||
implementation(libs.serialization.json)
|
||||
api(projects.libraries.oidc.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -39,13 +40,8 @@ dependencies {
|
|||
implementation(projects.services.toolbox.api)
|
||||
api(projects.libraries.permissions.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -17,10 +19,5 @@ dependencies {
|
|||
implementation(projects.libraries.architecture)
|
||||
api(projects.libraries.permissions.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -18,7 +20,6 @@ dependencies {
|
|||
implementation(projects.libraries.matrix.api)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.coroutines.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ fun aRoomMember(
|
|||
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous: Boolean = false,
|
||||
powerLevel: Long = 0L,
|
||||
normalizedPowerLevel: Long = 0L,
|
||||
isIgnored: Boolean = false,
|
||||
role: RoomMember.Role = RoomMember.Role.User,
|
||||
membershipChangeReason: String? = null,
|
||||
|
|
@ -30,7 +29,6 @@ fun aRoomMember(
|
|||
membership = membership,
|
||||
isNameAmbiguous = isNameAmbiguous,
|
||||
powerLevel = powerLevel,
|
||||
normalizedPowerLevel = normalizedPowerLevel,
|
||||
isIgnored = isIgnored,
|
||||
role = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -63,20 +64,14 @@ dependencies {
|
|||
implementation(projects.services.appnavstate.api)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.coil.test)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.libraries.pushstore.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.features.call.test)
|
||||
testImplementation(projects.features.lockscreen.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
|
|||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
|
|
@ -58,13 +58,13 @@ class DefaultCallNotificationEventResolver(
|
|||
notificationData: NotificationData,
|
||||
forceNotify: Boolean
|
||||
): Result<NotifiableEvent> = runCatchingExceptions {
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.RtcNotification
|
||||
?: throw NotificationResolverException.UnknownError("content is not a call notify")
|
||||
|
||||
val previousRingingCallStatus = appForegroundStateService.hasRingingCall.value
|
||||
// We need the sync service working to get the updated room info
|
||||
val isRoomCallActive = runCatchingExceptions {
|
||||
if (content.type == CallNotifyType.RING) {
|
||||
if (content.type == RtcNotificationType.RING) {
|
||||
appForegroundStateService.updateHasRingingCall(true)
|
||||
|
||||
val client = clientProvider.getOrRestore(
|
||||
|
|
@ -90,7 +90,7 @@ class DefaultCallNotificationEventResolver(
|
|||
}.getOrDefault(false)
|
||||
|
||||
notificationData.run {
|
||||
if (content.type == CallNotifyType.RING && isRoomCallActive && !forceNotify) {
|
||||
if (content.type == RtcNotificationType.RING && isRoomCallActive && !forceNotify) {
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
|
|
@ -104,9 +104,10 @@ class DefaultCallNotificationEventResolver(
|
|||
description = stringProvider.getString(R.string.notification_incoming_call),
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
callNotifyType = content.type,
|
||||
rtcNotificationType = content.type,
|
||||
senderId = content.senderId,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
expirationTimestamp = content.expirationTimestampMillis,
|
||||
)
|
||||
} else {
|
||||
Timber.d("Event $eventId is call notify but should not ring: $isRoomCallActive, notify: ${content.type}")
|
||||
|
|
@ -124,7 +125,7 @@ class DefaultCallNotificationEventResolver(
|
|||
roomIsDm = isDm,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
type = EventType.RTC_NOTIFICATION,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ class DefaultNotifiableEventResolver(
|
|||
)
|
||||
ResolvedPushEvent.Event(notifiableMessageEvent)
|
||||
}
|
||||
is NotificationContent.MessageLike.CallNotify -> {
|
||||
is NotificationContent.MessageLike.RtcNotification -> {
|
||||
val notifiableEvent = callNotificationEventResolver.resolveEvent(userId, this).getOrThrow()
|
||||
ResolvedPushEvent.Event(notifiableEvent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class DefaultNotificationCreator(
|
|||
|
||||
val smallIcon = CommonDrawables.ic_notification
|
||||
|
||||
val containsMissedCall = events.any { it.type == EventType.CALL_NOTIFY }
|
||||
val containsMissedCall = events.any { it.type == EventType.RTC_NOTIFICATION }
|
||||
val channelId = if (containsMissedCall) {
|
||||
notificationChannels.getChannelForIncomingCall(false)
|
||||
} else {
|
||||
|
|
@ -213,8 +213,8 @@ class DefaultNotificationCreator(
|
|||
}
|
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
|
||||
// If any of the events are of call notify type it means a missed call, set the category to the right value
|
||||
if (events.any { it.type == EventType.CALL_NOTIFY }) {
|
||||
// If any of the events are of rtc notification type it means a missed call, set the category to the right value
|
||||
if (events.any { it.type == EventType.RTC_NOTIFICATION }) {
|
||||
setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
|
||||
data class NotifiableRingingCallEvent(
|
||||
override val sessionId: SessionId,
|
||||
|
|
@ -27,6 +27,7 @@ data class NotifiableRingingCallEvent(
|
|||
val senderDisambiguatedDisplayName: String?,
|
||||
val senderAvatarUrl: String?,
|
||||
val roomAvatarUrl: String? = null,
|
||||
val callNotifyType: CallNotifyType,
|
||||
val rtcNotificationType: RtcNotificationType,
|
||||
val timestamp: Long,
|
||||
val expirationTimestamp: Long,
|
||||
) : NotifiableEvent
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import io.element.android.features.call.api.ElementCallEntryPoint
|
|||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||
import io.element.android.libraries.push.impl.history.onDiagnosticPush
|
||||
|
|
@ -58,7 +57,6 @@ class DefaultPushHandler(
|
|||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
private val diagnosticPushHandler: DiagnosticPushHandler,
|
||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
|
|
@ -241,32 +239,15 @@ class DefaultPushHandler(
|
|||
} else {
|
||||
Timber.tag(loggerTag.value).d("## handleInternal()")
|
||||
}
|
||||
val clientSecret = pushData.clientSecret
|
||||
// clientSecret should not be null. If this happens, restore default session
|
||||
var reason = if (clientSecret == null) "No client secret" else ""
|
||||
val userId = clientSecret?.let {
|
||||
// Get userId from client secret
|
||||
pushClientSecret.getUserIdFromSecret(clientSecret).also {
|
||||
if (it == null) {
|
||||
reason = "Unable to get userId from client secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
matrixAuthenticationService.getLatestSessionId().also {
|
||||
if (it == null) {
|
||||
if (reason.isNotEmpty()) reason += " - "
|
||||
reason += "Unable to get latest sessionId"
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get userId from client secret
|
||||
val userId = pushClientSecret.getUserIdFromSecret(pushData.clientSecret)
|
||||
if (userId == null) {
|
||||
Timber.w("Unable to get a session")
|
||||
Timber.w("Unable to get userId from client secret")
|
||||
pushHistoryService.onUnableToRetrieveSession(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
reason = reason,
|
||||
reason = "Unable to get userId from client secret",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
|
@ -296,6 +277,7 @@ class DefaultPushHandler(
|
|||
senderName = notifiableEvent.senderDisambiguatedDisplayName,
|
||||
avatarUrl = notifiableEvent.roomAvatarUrl,
|
||||
timestamp = notifiableEvent.timestamp,
|
||||
expirationTimestamp = notifiableEvent.expirationTimestamp,
|
||||
notificationChannelId = notificationChannels.getChannelForIncomingCall(ring = true),
|
||||
textContent = notifiableEvent.description,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
<item quantity="other">"%d convites"</item>
|
||||
</plurals>
|
||||
<string name="notification_invite_body">"Convidou-te para conversar"</string>
|
||||
<string name="notification_invite_body_with_sender">"%1$s convidou-o para conversar"</string>
|
||||
<string name="notification_invite_body_with_sender">"%1$s convidou-te para conversar"</string>
|
||||
<string name="notification_mentioned_you_body">"Mencionou-te: %1$s"</string>
|
||||
<string name="notification_new_messages">"Mensagens novas"</string>
|
||||
<plurals name="notification_new_messages_for_room">
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
<string name="notification_room_action_mark_as_read">"Marcar como lida"</string>
|
||||
<string name="notification_room_action_quick_reply">"Resposta rápida"</string>
|
||||
<string name="notification_room_invite_body">"Convidou-te a entrar na sala"</string>
|
||||
<string name="notification_room_invite_body_with_sender">"%1$s convidou-o a juntar-se à sala"</string>
|
||||
<string name="notification_room_invite_body_with_sender">"%1$s convidou-te a entrares na sala"</string>
|
||||
<string name="notification_sender_me">"Eu"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s mencionou ou respondeu"</string>
|
||||
<string name="notification_test_push_notification_content">"Estás a ver a notificação! Clica em mim!"</string>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
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_ROOM_NAME
|
||||
|
|
@ -61,11 +61,12 @@ class DefaultCallNotificationEventResolverTest {
|
|||
isUpdated = false,
|
||||
senderDisambiguatedDisplayName = A_USER_NAME_2,
|
||||
senderAvatarUrl = null,
|
||||
callNotifyType = CallNotifyType.RING,
|
||||
expirationTimestamp = 1567L,
|
||||
rtcNotificationType = RtcNotificationType.RING,
|
||||
)
|
||||
|
||||
val notificationData = aNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.RING)
|
||||
content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.RING, 1567)
|
||||
)
|
||||
val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
|
|
@ -105,11 +106,11 @@ class DefaultCallNotificationEventResolverTest {
|
|||
imageUriString = null,
|
||||
imageMimeType = null,
|
||||
threadId = null,
|
||||
type = "m.call.notify",
|
||||
type = "org.matrix.msc4075.rtc.notification",
|
||||
)
|
||||
|
||||
val notificationData = aNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.NOTIFY)
|
||||
content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.NOTIFY, 0)
|
||||
)
|
||||
val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
|
|
@ -149,11 +150,11 @@ class DefaultCallNotificationEventResolverTest {
|
|||
imageUriString = null,
|
||||
imageMimeType = null,
|
||||
threadId = null,
|
||||
type = "m.call.notify",
|
||||
type = "org.matrix.msc4075.rtc.notification",
|
||||
)
|
||||
|
||||
val notificationData = aNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(A_USER_ID_2, CallNotifyType.RING)
|
||||
content = NotificationContent.MessageLike.RtcNotification(A_USER_ID_2, RtcNotificationType.RING, 0)
|
||||
)
|
||||
val result = resolver.resolveEvent(A_SESSION_ID, notificationData)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
|
|
@ -693,9 +693,10 @@ class DefaultNotifiableEventResolverTest {
|
|||
notificationResult = Result.success(
|
||||
mapOf(
|
||||
AN_EVENT_ID to Result.success(aNotificationData(
|
||||
content = NotificationContent.MessageLike.CallNotify(
|
||||
content = NotificationContent.MessageLike.RtcNotification(
|
||||
A_USER_ID_2,
|
||||
CallNotifyType.NOTIFY
|
||||
RtcNotificationType.NOTIFY,
|
||||
0
|
||||
),
|
||||
))
|
||||
)
|
||||
|
|
@ -719,7 +720,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
isRedacted = false,
|
||||
imageUriString = null,
|
||||
imageMimeType = null,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
type = EventType.RTC_NOTIFICATION,
|
||||
)
|
||||
)
|
||||
callNotificationEventResolver.resolveEventLambda = { _, _, _ -> Result.success(expectedResult.notifiableEvent) }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
|
|
@ -119,8 +119,9 @@ fun aNotifiableCallEvent(
|
|||
senderName: String? = null,
|
||||
roomAvatarUrl: String? = AN_AVATAR_URL,
|
||||
senderAvatarUrl: String? = AN_AVATAR_URL,
|
||||
callNotifyType: CallNotifyType = CallNotifyType.NOTIFY,
|
||||
rtcNotificationType: RtcNotificationType = RtcNotificationType.NOTIFY,
|
||||
timestamp: Long = 0L,
|
||||
expirationTimestamp: Long = 0L,
|
||||
) = NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
eventId = eventId,
|
||||
|
|
@ -129,6 +130,7 @@ fun aNotifiableCallEvent(
|
|||
editedEventId = null,
|
||||
description = "description",
|
||||
timestamp = timestamp,
|
||||
expirationTimestamp = expirationTimestamp,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
|
|
@ -136,5 +138,5 @@ fun aNotifiableCallEvent(
|
|||
senderId = senderId,
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
callNotifyType = callNotifyType,
|
||||
rtcNotificationType = rtcNotificationType,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,13 +14,12 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.test.FakeElementCallEntryPoint
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.RtcNotificationType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
|
|
@ -28,7 +27,6 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|||
import io.element.android.libraries.matrix.test.A_SECRET
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.push.impl.history.FakePushHistoryService
|
||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||
|
|
@ -181,7 +179,7 @@ class DefaultPushHandlerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `when PushData is received, but client secret is not known, fallback the latest session`() =
|
||||
fun `when PushData is received, but client secret is not known, nothing happen`() =
|
||||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
|
|
@ -207,58 +205,6 @@ class DefaultPushHandlerTest {
|
|||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { null }
|
||||
),
|
||||
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
|
||||
getLatestSessionIdLambda = { A_USER_ID }
|
||||
},
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
advanceTimeBy(300.milliseconds)
|
||||
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_USER_ID), any())
|
||||
onNotifiableEventsReceived.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(listOf(aNotifiableMessageEvent)))
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when PushData is received, but client secret is not known, and there is no latest session, nothing happen`() =
|
||||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
|
||||
}
|
||||
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = notifiableEventResult,
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { null }
|
||||
),
|
||||
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
|
||||
getLatestSessionIdLambda = { null }
|
||||
},
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
|
|
@ -388,7 +334,7 @@ class DefaultPushHandlerTest {
|
|||
mapOf(
|
||||
request to Result.success(
|
||||
ResolvedPushEvent.Event(
|
||||
aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli())
|
||||
aNotifiableCallEvent(rtcNotificationType = RtcNotificationType.RING, timestamp = Instant.now().toEpochMilli())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -440,7 +386,7 @@ class DefaultPushHandlerTest {
|
|||
onNotifiableEventsReceived = onNotifiableEventsReceived,
|
||||
notifiableEventsResult = { _, _ ->
|
||||
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY)))))
|
||||
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.RTC_NOTIFICATION)))))
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
|
|
@ -655,8 +601,8 @@ class DefaultPushHandlerTest {
|
|||
var receivedFallbackEvent = false
|
||||
val onPushReceivedResult =
|
||||
lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, isResolved, _, comment ->
|
||||
receivedFallbackEvent = !isResolved && comment == "Unable to resolve event: ${aNotifiableFallbackEvent.cause}"
|
||||
}
|
||||
receivedFallbackEvent = !isResolved && comment == "Unable to resolve event: ${aNotifiableFallbackEvent.cause}"
|
||||
}
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
|
|
@ -694,7 +640,6 @@ class DefaultPushHandlerTest {
|
|||
userPushStore: UserPushStore = FakeUserPushStore(),
|
||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||
diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(),
|
||||
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
|
||||
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
|
||||
|
|
@ -712,7 +657,6 @@ class DefaultPushHandlerTest {
|
|||
userPushStoreFactory = FakeUserPushStoreFactory { userPushStore },
|
||||
pushClientSecret = pushClientSecret,
|
||||
buildMeta = buildMeta,
|
||||
matrixAuthenticationService = matrixAuthenticationService,
|
||||
diagnosticPushHandler = diagnosticPushHandler,
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notificationChannels = notificationChannels,
|
||||
|
|
|
|||
|
|
@ -22,5 +22,5 @@ data class PushData(
|
|||
val eventId: EventId,
|
||||
val roomId: RoomId,
|
||||
val unread: Int?,
|
||||
val clientSecret: String?,
|
||||
val clientSecret: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import config.BuildTimeConfig
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
|
|
@ -68,17 +69,12 @@ dependencies {
|
|||
exclude(group = "com.google.firebase", module = "firebase-measurement-connector")
|
||||
}
|
||||
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.kotlinx.collections.immutable)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushstore.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,10 +34,11 @@ data class PushDataFirebase(
|
|||
fun PushDataFirebase.toPushData(): PushData? {
|
||||
val safeEventId = eventId?.let(::EventId) ?: return null
|
||||
val safeRoomId = roomId?.let(::RoomId) ?: return null
|
||||
val safeClientSecret = clientSecret ?: return null
|
||||
return PushData(
|
||||
eventId = safeEventId,
|
||||
roomId = safeRoomId,
|
||||
unread = unread,
|
||||
clientSecret = clientSecret,
|
||||
clientSecret = safeClientSecret,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ class FirebasePushParserTest {
|
|||
assertThrowsInDebug { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test empty client secret`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("cs", null))).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test invalid eventId`() {
|
||||
val pushParser = FirebasePushParser()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -42,18 +43,14 @@ dependencies {
|
|||
|
||||
// UnifiedPush library
|
||||
api(libs.unifiedpush)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.kotlinx.collections.immutable)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.libraries.pushstore.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -31,12 +32,7 @@ dependencies {
|
|||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -28,11 +29,6 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.roomselect.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
|
|||
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