Merge pull request #3023 from element-hq/feature/fga/update_rust_sdk_0.2.25

Feature/fga/update rust sdk 0.2.25
This commit is contained in:
ganfra 2024-06-13 14:07:10 +02:00 committed by GitHub
commit 5b18f9cddd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
184 changed files with 656 additions and 1047 deletions

View file

@ -41,6 +41,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.loggedin.SendQueues
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
@ -102,6 +103,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
private val shareEntryPoint: ShareEntryPoint,
private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues,
snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
@ -157,6 +159,11 @@ class LoggedInFlowNode @AssistedInject constructor(
}
)
observeSyncStateAndNetworkStatus()
setupSendingQueue()
}
private fun setupSendingQueue() {
sendingQueue.launchIn(lifecycleScope)
}
@OptIn(FlowPreview::class)

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.appnav.loggedin
import androidx.annotation.VisibleForTesting
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
@VisibleForTesting
const val SEND_QUEUES_RETRY_DELAY_MILLIS = 1500L
@SingleIn(SessionScope::class)
class SendQueues @Inject constructor(
private val matrixClient: MatrixClient,
private val networkMonitor: NetworkMonitor,
) {
fun launchIn(coroutineScope: CoroutineScope) {
networkMonitor.connectivity
.onEach { networkStatus ->
matrixClient.setAllSendQueuesEnabled(enabled = networkStatus == NetworkStatus.Online)
}
.launchIn(coroutineScope)
matrixClient.sendQueueDisabledFlow()
.onEach { roomId: RoomId ->
Timber.d("Send queue disabled for room $roomId")
if (networkMonitor.connectivity.value == NetworkStatus.Online) {
delay(SEND_QUEUES_RETRY_DELAY_MILLIS)
matrixClient.getRoom(roomId)?.use { room ->
room.setSendQueueEnabled(enabled = true)
}
}
}
.launchIn(coroutineScope)
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.appnav.loggedin
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class) class SendQueuesTest {
private val matrixClient = FakeMatrixClient()
private val room = FakeMatrixRoom()
private val networkMonitor = FakeNetworkMonitor()
private val sut = SendQueues(matrixClient, networkMonitor)
@Test
fun `test network status online and sending queue failed`() = runTest {
val sendQueueDisabledFlow = MutableSharedFlow<RoomId>(replay = 1)
val setAllSendQueuesEnabledLambda = lambdaRecorder { _: Boolean -> }
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
matrixClient.givenGetRoomResult(room.roomId, room)
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
room.setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
sut.launchIn(backgroundScope)
sendQueueDisabledFlow.emit(room.roomId)
advanceTimeBy(SEND_QUEUES_RETRY_DELAY_MILLIS)
runCurrent()
assert(setAllSendQueuesEnabledLambda)
.isCalledOnce()
.with(value(true))
assert(setRoomSendQueueEnabledLambda)
.isCalledOnce()
.with(value(true))
}
@Test
fun `test network status offline and sending queue failed`() = runTest {
val sendQueueDisabledFlow = MutableSharedFlow<RoomId>(replay = 1)
val setAllSendQueuesEnabledLambda = lambdaRecorder { _: Boolean -> }
matrixClient.sendQueueDisabledFlow = sendQueueDisabledFlow
matrixClient.setAllSendQueuesEnabledLambda = setAllSendQueuesEnabledLambda
networkMonitor.connectivity.value = NetworkStatus.Offline
matrixClient.givenGetRoomResult(room.roomId, room)
val setRoomSendQueueEnabledLambda = lambdaRecorder { _: Boolean -> }
room.setSendQueueEnabledLambda = setRoomSendQueueEnabledLambda
sut.launchIn(backgroundScope)
sendQueueDisabledFlow.emit(room.roomId)
advanceTimeBy(SEND_QUEUES_RETRY_DELAY_MILLIS)
runCurrent()
assert(setAllSendQueuesEnabledLambda)
.isCalledOnce()
.with(value(false))
assert(setRoomSendQueueEnabledLambda)
.isNeverCalled()
}
@Test
fun `test network status getting offline and online`() = runTest {
val setEnableSendingQueueLambda = lambdaRecorder { _: Boolean -> }
matrixClient.setAllSendQueuesEnabledLambda = setEnableSendingQueueLambda
sut.launchIn(backgroundScope)
advanceTimeBy(SEND_QUEUES_RETRY_DELAY_MILLIS)
networkMonitor.connectivity.value = NetworkStatus.Offline
advanceTimeBy(SEND_QUEUES_RETRY_DELAY_MILLIS)
networkMonitor.connectivity.value = NetworkStatus.Online
advanceTimeBy(SEND_QUEUES_RETRY_DELAY_MILLIS)
assert(setEnableSendingQueueLambda)
.isCalledExactly(3)
.withSequence(
listOf(value(true)),
listOf(value(false)),
listOf(value(true)),
)
}
}

View file

@ -46,7 +46,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
@ -107,7 +106,6 @@ class MessagesPresenter @AssistedInject constructor(
private val actionListPresenter: ActionListPresenter,
private val customReactionPresenter: CustomReactionPresenter,
private val reactionSummaryPresenter: ReactionSummaryPresenter,
private val retrySendMenuPresenter: RetrySendMenuPresenter,
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher,
@ -140,7 +138,6 @@ class MessagesPresenter @AssistedInject constructor(
val actionListState = actionListPresenter.present()
val customReactionState = customReactionPresenter.present()
val reactionSummaryState = reactionSummaryPresenter.present()
val retryState = retrySendMenuPresenter.present()
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
@ -231,7 +228,6 @@ class MessagesPresenter @AssistedInject constructor(
actionListState = actionListState,
customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState,
retrySendMenuState = retryState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
snackbarMessage = snackbarMessage,
@ -309,11 +305,9 @@ class MessagesPresenter @AssistedInject constructor(
}
private suspend fun handleActionRedact(event: TimelineItem.Event) {
if (event.failedToSend) {
// If the message hasn't been sent yet, just cancel it
event.transactionId?.let { room.cancelSend(it) }
} else if (event.eventId != null) {
room.redactEvent(event.eventId)
timelineController.invokeOnCurrentTimeline {
redactEvent(eventId = event.eventId, transactionId = event.transactionId, reason = null)
.onFailure { Timber.e(it) }
}
}

View file

@ -23,7 +23,6 @@ import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
import io.element.android.libraries.architecture.AsyncData
@ -47,7 +46,6 @@ data class MessagesState(
val actionListState: ActionListState,
val customReactionState: CustomReactionState,
val reactionSummaryState: ReactionSummaryState,
val retrySendMenuState: RetrySendMenuState,
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,

View file

@ -31,8 +31,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.aRetrySendMenuState
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.typing.aTypingNotificationState
@ -110,7 +108,6 @@ fun aMessagesState(
// Render a focused event for an event with sender information displayed
focusedEventIndex = 2,
),
retrySendMenuState: RetrySendMenuState = aRetrySendMenuState(),
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
actionListState: ActionListState = anActionListState(),
customReactionState: CustomReactionState = aCustomReactionState(),
@ -132,7 +129,6 @@ fun aMessagesState(
voiceMessageComposerState = voiceMessageComposerState,
typingNotificationState = aTypingNotificationState(),
timelineState = timelineState,
retrySendMenuState = retrySendMenuState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
actionListState = actionListState,
customReactionState = customReactionState,

View file

@ -74,8 +74,6 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
@ -103,7 +101,6 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import timber.log.Timber
@ -212,11 +209,6 @@ fun MessagesView(
onMessageLongClick = ::onMessageLongClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onTimestampClick = { event ->
if (event.localSendState is LocalEventSendState.SendingFailed) {
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
}
},
onReactionClick = ::onEmojiReactionClick,
onReactionLongClick = ::onEmojiReactionLongClick,
onMoreReactionsClick = ::onMoreReactionsClick,
@ -258,7 +250,6 @@ fun MessagesView(
)
ReactionSummaryView(state = state.reactionSummaryState)
RetrySendMessageMenu(state = state.retrySendMenuState)
ReadReceiptBottomSheet(
state = state.readReceiptBottomSheetState,
onUserDataClick = onUserDataClick,
@ -319,7 +310,6 @@ private fun MessagesViewContent(
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onMessageLongClick: (TimelineItem.Event) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onSendLocationClick: () -> Unit,
onCreatePollClick: () -> Unit,
onJoinCallClick: () -> Unit,
@ -387,7 +377,6 @@ private fun MessagesViewContent(
onLinkClick = onLinkClick,
onMessageClick = onMessageClick,
onMessageLongClick = onMessageLongClick,
onTimestampClick = onTimestampClick,
onSwipeToReply = onSwipeToReply,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,

View file

@ -181,7 +181,7 @@ class ActionListPresenter @Inject constructor(
add(TimelineItemAction.Forward)
}
}
if (timelineItem.isMine && timelineItem.isTextMessage) {
if (timelineItem.isEditable) {
add(TimelineItemAction.Edit)
}
if (timelineItem.content.canBeCopied()) {

View file

@ -83,7 +83,6 @@ fun TimelineView(
onLinkClick: (String) -> Unit,
onMessageClick: (TimelineItem.Event) -> Unit,
onMessageLongClick: (TimelineItem.Event) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
@ -148,7 +147,6 @@ fun TimelineView(
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
onTimestampClick = onTimestampClick,
eventSink = state.eventSink,
onSwipeToReply = onSwipeToReply,
onJoinCallClick = onJoinCallClick,
@ -245,8 +243,8 @@ private fun BoxScope.TimelineScrollHelper(
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 12.dp),
onClick = { jumpToBottom() },
)
}
@ -273,8 +271,8 @@ private fun JumpToBottomButton(
) {
Icon(
modifier = Modifier
.size(24.dp)
.rotate(90f),
.size(24.dp)
.rotate(90f),
imageVector = CompoundIcons.ArrowRight(),
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
)
@ -301,7 +299,6 @@ internal fun TimelineViewPreview(
onLinkClick = {},
onMessageClick = {},
onMessageLongClick = {},
onTimestampClick = {},
onSwipeToReply = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },

View file

@ -45,6 +45,5 @@ internal fun ATimelineItemEventRow(
onMoreReactionsClick = {},
onReadReceiptClick = {},
onSwipeToReply = {},
onTimestampClick = {},
eventSink = {},
)

View file

@ -16,61 +16,34 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.isEdited
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimelineEventTimestampView(
event: TimelineItem.Event,
onClick: () -> Unit,
onLongClick: () -> Unit,
formattedTime: String,
isMessageEdited: Boolean,
modifier: Modifier = Modifier,
) {
val formattedTime = event.sentTime
val hasMessageSendingFailed = event.localSendState is LocalEventSendState.SendingFailed
val isMessageEdited = event.content.isEdited()
val tint = if (hasMessageSendingFailed) MaterialTheme.colorScheme.error else null
val clickModifier = if (hasMessageSendingFailed) {
Modifier.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
indication = rememberRipple(bounded = false),
interactionSource = remember { MutableInteractionSource() }
)
} else {
Modifier
}
Row(
modifier = Modifier
.then(clickModifier)
// Add extra padding for touch target size
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
.then(modifier),
verticalAlignment = Alignment.CenterVertically,
@ -79,35 +52,22 @@ fun TimelineEventTimestampView(
Text(
stringResource(CommonStrings.common_edited_suffix),
style = ElementTheme.typography.fontBodyXsRegular,
color = tint ?: MaterialTheme.colorScheme.secondary,
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
formattedTime,
style = ElementTheme.typography.fontBodyXsRegular,
color = tint ?: MaterialTheme.colorScheme.secondary,
color = MaterialTheme.colorScheme.secondary,
)
if (hasMessageSendingFailed && tint != null) {
Spacer(modifier = Modifier.width(2.dp))
Icon(
imageVector = CompoundIcons.Error(),
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
tint = tint,
modifier = Modifier.size(15.dp, 18.dp),
)
}
}
}
@PreviewsDayNight
@Composable
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
TimelineEventTimestampView(
event = event,
onClick = {},
onLongClick = {},
)
TimelineEventTimestampView(formattedTime = event.sentTime, isMessageEdited = event.content.isEdited())
}
object TimelineEventTimestampViewDefaults {

View file

@ -92,6 +92,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
import io.element.android.features.messages.impl.timeline.model.event.isEdited
import io.element.android.features.messages.impl.timeline.model.eventId
import io.element.android.features.messages.impl.timeline.model.metadata
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
@ -130,7 +131,6 @@ fun TimelineItemEventRow(
onLinkClick: (String) -> Unit,
onUserDataClick: (UserId) -> Unit,
inReplyToClick: (EventId) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
@ -190,7 +190,6 @@ fun TimelineItemEventRow(
interactionSource = interactionSource,
onClick = onClick,
onLongClick = onLongClick,
onTimestampClick = onTimestampClick,
inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick,
onReactionClick = { emoji -> onReactionClick(emoji, event) },
@ -209,7 +208,6 @@ fun TimelineItemEventRow(
interactionSource = interactionSource,
onClick = onClick,
onLongClick = onLongClick,
onTimestampClick = onTimestampClick,
inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick,
onReactionClick = { emoji -> onReactionClick(emoji, event) },
@ -265,7 +263,6 @@ private fun TimelineItemEventRowContent(
interactionSource: MutableInteractionSource,
onClick: () -> Unit,
onLongClick: () -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
inReplyToClick: () -> Unit,
onUserDataClick: () -> Unit,
onReactionClick: (emoji: String) -> Unit,
@ -337,9 +334,6 @@ private fun TimelineItemEventRowContent(
event = event,
onMessageLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onTimestampClick = {
onTimestampClick(event)
},
onLinkClick = onLinkClick,
eventSink = eventSink,
)
@ -419,7 +413,6 @@ private fun MessageEventBubbleContent(
event: TimelineItem.Event,
onMessageLongClick: () -> Unit,
inReplyToClick: () -> Unit,
onTimestampClick: () -> Unit,
onLinkClick: (String) -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
@SuppressLint("ModifierParameter")
@ -467,9 +460,8 @@ private fun MessageEventBubbleContent(
Box(modifier, contentAlignment = Alignment.Center) {
content {}
TimelineEventTimestampView(
event = event,
onClick = onTimestampClick,
onLongClick = ::onTimestampLongClick,
formattedTime = event.sentTime,
isMessageEdited = event.content.isEdited(),
modifier = Modifier
// Outer padding
.padding(horizontal = 4.dp, vertical = 4.dp)
@ -489,9 +481,8 @@ private fun MessageEventBubbleContent(
content = { content(this::onContentLayoutChange) },
overlay = {
TimelineEventTimestampView(
event = event,
onClick = onTimestampClick,
onLongClick = ::onTimestampLongClick,
formattedTime = event.sentTime,
isMessageEdited = event.content.isEdited(),
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
)
@ -501,9 +492,8 @@ private fun MessageEventBubbleContent(
Column(modifier) {
content {}
TimelineEventTimestampView(
event = event,
onClick = onTimestampClick,
onLongClick = ::onTimestampLongClick,
formattedTime = event.sentTime,
isMessageEdited = event.content.isEdited(),
modifier = Modifier
.align(Alignment.End)
.padding(horizontal = 8.dp, vertical = 4.dp)

View file

@ -49,7 +49,6 @@ fun TimelineItemGroupedEventsRow(
inReplyToClick: (EventId) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
@ -76,7 +75,6 @@ fun TimelineItemGroupedEventsRow(
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onTimestampClick = onTimestampClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
@ -100,7 +98,6 @@ private fun TimelineItemGroupedEventsRowContent(
inReplyToClick: (EventId) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
@ -133,7 +130,6 @@ private fun TimelineItemGroupedEventsRowContent(
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onTimestampClick = onTimestampClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
@ -175,7 +171,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
inReplyToClick = {},
onUserDataClick = {},
onLinkClick = {},
onTimestampClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},
@ -200,7 +195,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
inReplyToClick = {},
onUserDataClick = {},
onLinkClick = {},
onTimestampClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
onMoreReactionsClick = {},

View file

@ -54,7 +54,6 @@ internal fun TimelineItemRow(
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
onReadReceiptClick: (TimelineItem.Event) -> Unit,
onTimestampClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit,
onJoinCallClick: () -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
@ -118,7 +117,6 @@ internal fun TimelineItemRow(
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
onTimestampClick = onTimestampClick,
onSwipeToReply = { onSwipeToReply(timelineItem) },
eventSink = eventSink,
)
@ -137,7 +135,6 @@ internal fun TimelineItemRow(
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onTimestampClick = onTimestampClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,

View file

@ -81,7 +81,8 @@ fun TimelineItemReadReceiptView(
}
} else {
when (state.sendState) {
LocalEventSendState.NotSentYet -> {
is LocalEventSendState.SendingFailed,
is LocalEventSendState.NotSentYet -> {
ReadReceiptsRow(modifier) {
Icon(
modifier = Modifier.padding(2.dp),
@ -91,10 +92,6 @@ fun TimelineItemReadReceiptView(
)
}
}
LocalEventSendState.Canceled -> Unit
is LocalEventSendState.SendingFailed -> {
// Error? The timestamp is already displayed in red
}
null,
is LocalEventSendState.Sent -> {
if (state.isLastOutgoingMessage) {

View file

@ -123,7 +123,6 @@ private fun ReadReceiptBottomSheetContent(
@PreviewsDayNight
@Composable
internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) = ElementPreview {
// TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed
Column {
ReadReceiptBottomSheetContent(
state = state,

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
import io.element.android.features.messages.impl.timeline.model.TimelineItem
sealed interface RetrySendMenuEvents {
data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents
data object Retry : RetrySendMenuEvents
data object Remove : RetrySendMenuEvents
data object Dismiss : RetrySendMenuEvents
}

View file

@ -1,71 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom
import kotlinx.coroutines.launch
import javax.inject.Inject
class RetrySendMenuPresenter @Inject constructor(
private val room: MatrixRoom,
) : Presenter<RetrySendMenuState> {
@Composable
override fun present(): RetrySendMenuState {
val coroutineScope = rememberCoroutineScope()
var selectedEvent: TimelineItem.Event? by remember { mutableStateOf(null) }
fun handleEvent(event: RetrySendMenuEvents) {
when (event) {
is RetrySendMenuEvents.EventSelected -> {
selectedEvent = event.event
}
RetrySendMenuEvents.Retry -> {
coroutineScope.launch {
selectedEvent?.transactionId?.let { transactionId ->
room.retrySendMessage(transactionId)
}
selectedEvent = null
}
}
RetrySendMenuEvents.Remove -> {
coroutineScope.launch {
selectedEvent?.transactionId?.let { transactionId ->
room.cancelSend(transactionId)
}
selectedEvent = null
}
}
RetrySendMenuEvents.Dismiss -> {
selectedEvent = null
}
}
}
return RetrySendMenuState(
selectedEvent = selectedEvent,
eventSink = { handleEvent(it) },
)
}
}

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.TimelineItem
@Immutable
data class RetrySendMenuState(
val selectedEvent: TimelineItem.Event?,
val eventSink: (RetrySendMenuEvents) -> Unit,
)

View file

@ -1,36 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.model.TimelineItem
class RetrySendMenuStateProvider : PreviewParameterProvider<RetrySendMenuState> {
override val values: Sequence<RetrySendMenuState> = sequenceOf(
aRetrySendMenuState(),
aRetrySendMenuState(event = aTimelineItemEvent()),
)
}
fun aRetrySendMenuState(
event: TimelineItem.Event? = null,
eventSink: (RetrySendMenuEvents) -> Unit = {},
) = RetrySendMenuState(
selectedEvent = event,
eventSink = eventSink,
)

View file

@ -1,159 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.R
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@Composable
internal fun RetrySendMessageMenu(
state: RetrySendMenuState,
modifier: Modifier = Modifier,
) {
val isVisible = state.selectedEvent != null
fun onDismiss() {
state.eventSink(RetrySendMenuEvents.Dismiss)
}
fun onRetry() {
state.eventSink(RetrySendMenuEvents.Retry)
}
fun onRemove() {
state.eventSink(RetrySendMenuEvents.Remove)
}
RetrySendMessageMenuBottomSheet(
modifier = modifier,
isVisible = isVisible,
onRetry = ::onRetry,
onRemove = ::onRemove,
onDismiss = ::onDismiss
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RetrySendMessageMenuBottomSheet(
isVisible: Boolean,
onRetry: () -> Unit,
onRemove: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val sheetState = rememberModalBottomSheetState()
val coroutineScope = rememberCoroutineScope()
if (isVisible) {
ModalBottomSheet(
modifier = modifier,
// modifier = modifier.navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
// .imePadding()
sheetState = sheetState,
onDismissRequest = {
coroutineScope.launch {
sheetState.hide()
onDismiss()
}
}
) {
RetrySendMenuContents(
onRetry = onRetry,
onRemove = onRemove,
)
// FIXME remove after https://issuetracker.google.com/issues/275849044
Spacer(modifier = Modifier.height(32.dp))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ColumnScope.RetrySendMenuContents(
onRetry: () -> Unit,
onRemove: () -> Unit,
sheetState: SheetState = rememberModalBottomSheetState(),
) {
val coroutineScope = rememberCoroutineScope()
ListItem(headlineContent = {
Text(
text = stringResource(R.string.screen_room_retry_send_menu_title),
style = ElementTheme.typography.fontBodyLgMedium,
)
})
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.screen_room_retry_send_menu_send_again_action),
style = ElementTheme.typography.fontBodyLgRegular,
)
},
modifier = Modifier.clickable {
coroutineScope.launch {
sheetState.hide()
onRetry()
}
}
)
ListItem(
headlineContent = {
Text(
text = stringResource(CommonStrings.action_remove),
style = ElementTheme.typography.fontBodyLgRegular,
)
},
colors = ListItemDefaults.colors(headlineColor = MaterialTheme.colorScheme.error),
modifier = Modifier.clickable {
coroutineScope.launch {
sheetState.hide()
onRemove()
}
}
)
}
@PreviewsDayNight
@Composable
internal fun RetrySendMessageMenuPreview(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) = ElementPreview {
RetrySendMessageMenu(
state = state,
)
}

View file

@ -38,7 +38,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact
import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
@ -64,19 +63,18 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
@ -445,41 +443,25 @@ class MessagesPresenterTest {
@Test
fun `present - handle action redact`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
val matrixRoom = FakeMatrixRoom()
val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
}
}
@Test
fun `present - handle action redact message in error, in this case the message is just cancelled`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
val matrixRoom = FakeMatrixRoom()
val liveTimeline = FakeTimeline()
val matrixRoom = FakeMatrixRoom(liveTimeline = liveTimeline)
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
liveTimeline.redactEventLambda = redactEventLambda
val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(
MessagesEvents.HandleAction(
action = TimelineItemAction.Redact,
event = aMessageEvent(
transactionId = A_TRANSACTION_ID,
sendState = LocalEventSendState.SendingFailed("Failed to send message")
)
)
)
assertThat(matrixRoom.cancelSendCount).isEqualTo(1)
assertThat(matrixRoom.redactEventEventIdParam).isNull()
val messageEvent = aMessageEvent()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assert(redactEventLambda)
.isCalledOnce()
.with(value(messageEvent.eventId), value(messageEvent.transactionId), value(null))
}
}
@ -839,7 +821,6 @@ class MessagesPresenterTest {
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
return MessagesPresenter(
room = matrixRoom,
composerPresenter = messageComposerPresenter,
@ -849,7 +830,6 @@ class MessagesPresenterTest {
actionListPresenter = actionListPresenter,
customReactionPresenter = customReactionPresenter,
reactionSummaryPresenter = reactionSummaryPresenter,
retrySendMenuPresenter = retrySendMenuPresenter,
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
networkMonitor = FakeNetworkMonitor(),
snackbarDispatcher = SnackbarDispatcher(),

View file

@ -52,8 +52,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.aRetrySendMenuState
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.libraries.matrix.api.core.UserId
@ -74,6 +72,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class MessagesViewTest {
@ -145,22 +144,6 @@ class MessagesViewTest {
callback.assertSuccess()
}
@Test
fun `clicking on an Event timestamp in error emits the expected Event`() {
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
val state = aMessagesState(
retrySendMenuState = aRetrySendMenuState(
eventSink = eventsRecorder
),
)
val timelineItem = state.timelineState.timelineItems[1] as TimelineItem.Event
rule.setMessagesView(
state = state,
)
rule.onAllNodesWithText(timelineItem.sentTime)[1].performClick()
eventsRecorder.assertSingle(RetrySendMenuEvents.EventSelected(timelineItem))
}
@Test
fun `long clicking on an Event emits the expected Event userHasPermissionToSendMessage`() {
`long clicking on an Event emits the expected Event`(userHasPermissionToSendMessage = true)
@ -313,6 +296,7 @@ class MessagesViewTest {
}
@Test
@Config(qualifiers = "h1024dp")
fun `clicking on the sender of an Event invoke expected callback`() {
val eventsRecorder = EventsRecorder<MessagesEvents>(expectEvents = false)
val state = aMessagesState(

View file

@ -62,7 +62,7 @@ class ActionListPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
val messageEvent = aMessageEvent(isMine = true, isEditable = false, content = TimelineItemRedactedContent)
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
@ -96,7 +96,11 @@ class ActionListPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemRedactedContent
)
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
@ -132,6 +136,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
@ -174,6 +179,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
@ -215,6 +221,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
@ -256,6 +263,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = null)
)
initialState.eventSink.invoke(
@ -382,6 +390,7 @@ class ActionListPresenterTest {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = true,
isEditable = false,
content = aTimelineItemImageContent(),
)
initialState.eventSink.invoke(

View file

@ -106,7 +106,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
onMessageClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onMessageLongClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onTimestampClick: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onSwipeToReply: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit = EnsureNeverCalledWithTwoParams(),
@ -123,7 +122,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
onLinkClick = onLinkClick,
onMessageClick = onMessageClick,
onMessageLongClick = onMessageLongClick,
onTimestampClick = onTimestampClick,
onSwipeToReply = onSwipeToReply,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,

View file

@ -1,163 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class RetrySendMenuPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
private val room = FakeMatrixRoom()
private val presenter = RetrySendMenuPresenter(room)
@Test
fun `present - handle event selected`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent()
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent)
}
}
@Test
fun `present - handle dismiss`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent()
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Dismiss)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle resend with transactionId`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Retry)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(1)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle resend without transactionId`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = null)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Retry)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle resend with error`() = runTest {
room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error")))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Retry)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(1)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle remove failed message with transactionId`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Remove)
assertThat(room.cancelSendCount).isEqualTo(1)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle remove failed message without transactionId`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = null)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Remove)
assertThat(room.cancelSendCount).isEqualTo(0)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
@Test
fun `present - handle remove failed message with error`() = runTest {
room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error")))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID)
initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent))
skipItems(1)
initialState.eventSink(RetrySendMenuEvents.Remove)
assertThat(room.cancelSendCount).isEqualTo(1)
assertThat(room.retrySendMessageCount).isEqualTo(0)
assertThat(awaitItem().selectedEvent).isNull()
}
}
}

View file

@ -1,90 +0,0 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.retrysendmenu
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class RetrySendMessageMenuTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `dismiss the bottom sheet emits the expected event`() {
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
rule.setRetrySendMessageMenu(
aRetrySendMenuState(
event = aTimelineItemEvent(),
eventSink = eventsRecorder
),
)
rule.pressBackKey()
// Cannot test this for now.
// eventsRecorder.assertSingle(RetrySendMenuEvents.Dismiss)
}
@Config(qualifiers = "h1024dp")
@Test
fun `retry to send the event emits the expected event`() {
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
rule.setRetrySendMessageMenu(
aRetrySendMenuState(
event = aTimelineItemEvent(),
eventSink = eventsRecorder
),
)
rule.clickOn(R.string.screen_room_retry_send_menu_send_again_action)
eventsRecorder.assertSingle(RetrySendMenuEvents.Retry)
}
@Config(qualifiers = "h1024dp")
@Test
fun `remove the event emits the expected event`() {
val eventsRecorder = EventsRecorder<RetrySendMenuEvents>()
rule.setRetrySendMessageMenu(
aRetrySendMenuState(
event = aTimelineItemEvent(),
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_remove)
eventsRecorder.assertSingle(RetrySendMenuEvents.Remove)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRetrySendMessageMenu(
state: RetrySendMenuState,
) {
setContent {
RetrySendMessageMenu(
state = state,
)
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.migration.impl.migrations
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.sessionstorage.api.SessionStore
import java.io.File
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
class AppMigration05 @Inject constructor(
private val sessionStore: SessionStore,
private val baseDirectory: File,
) : AppMigration {
override val order: Int = 5
override suspend fun migrate() {
val allSessions = sessionStore.getAllSessions()
for (session in allSessions) {
if (session.sessionPath.isEmpty()) {
val sessionPath = File(baseDirectory, session.userId.replace(':', '_')).absolutePath
sessionStore.updateData(session.copy(sessionPath = sessionPath))
}
}
}
}

View file

@ -68,8 +68,12 @@ class PollRepository @Inject constructor(
suspend fun deletePoll(
pollStartId: EventId,
): Result<Unit> =
room.redactEvent(
eventId = pollStartId,
)
): Result<Boolean> =
timelineProvider
.getActiveTimeline()
.redactEvent(
eventId = pollStartId,
transactionId = null,
reason = null,
)
}

View file

@ -29,6 +29,7 @@ import io.element.android.features.poll.impl.aPollTimelineItems
import io.element.android.features.poll.impl.anOngoingPollContent
import io.element.android.features.poll.impl.data.PollRepository
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
@ -39,6 +40,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -455,47 +457,53 @@ import org.junit.Test
@Test
fun `delete confirms`() = runTest {
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
timeline.redactEventLambda = redactEventLambda
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
awaitDeleteConfirmation()
assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull()
assert(redactEventLambda).isNeverCalled()
}
}
@Test
fun `delete can be cancelled`() = runTest {
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
timeline.redactEventLambda = redactEventLambda
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull()
awaitDeleteConfirmation().eventSink(CreatePollEvents.HideConfirmation)
awaitPollLoaded().apply {
assertThat(showDeleteConfirmation).isFalse()
}
assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull()
assert(redactEventLambda).isNeverCalled()
}
}
@Test
fun `delete can be confirmed`() = runTest {
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
timeline.redactEventLambda = redactEventLambda
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull()
awaitDeleteConfirmation().eventSink(CreatePollEvents.Delete(confirmed = true))
awaitPollLoaded().apply {
assertThat(showDeleteConfirmation).isFalse()
}
assertThat(fakeMatrixRoom.redactEventEventIdParam).isEqualTo(pollEventId)
assert(redactEventLambda)
.isCalledOnce()
.with(value(pollEventId), value(null), any())
}
}

View file

@ -287,7 +287,8 @@ class DefaultBugReporterTest {
oidcData = null,
refreshToken = null,
slidingSyncProxy = null,
passphrase = null
passphrase = null,
sessionPath = "session",
)
@Test
fun `test sendBugReport error`() = runTest {

View file

@ -51,5 +51,6 @@ fun aSessionData(
isTokenValid = isTokenValid,
loginType = LoginType.UNKNOWN,
passphrase = null,
sessionPath = "/a/path/to/a/session",
)
}

View file

@ -162,7 +162,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.24"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.25"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -105,4 +105,20 @@ interface MatrixClient : Closeable {
suspend fun getRecentlyVisitedRooms(): Result<List<RoomId>>
suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<ResolvedRoomAlias>
suspend fun getRoomPreviewFromRoomId(roomId: RoomId, serverNames: List<String>): Result<RoomPreview>
/**
* Enables or disables the sending queue, according to the given parameter.
*
* The sending queue automatically disables itself whenever sending an
* event with it failed (e.g. sending an event via the Timeline),
* so it's required to manually re-enable it as soon as
* connectivity is back on the device.
*/
suspend fun setAllSendQueuesEnabled(enabled: Boolean)
/**
* Returns a flow of room IDs that have send queue being disabled.
* This flow will emit a new value whenever the send queue is disabled for a room.
*/
fun sendQueueDisabledFlow(): Flow<RoomId>
}

View file

@ -130,8 +130,6 @@ interface MatrixRoom : Closeable {
suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
suspend fun sendImage(
file: File,
thumbnailFile: File?,
@ -160,7 +158,7 @@ interface MatrixRoom : Closeable {
suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit>
suspend fun cancelSend(transactionId: TransactionId): Result<Unit>
suspend fun cancelSend(transactionId: TransactionId): Result<Boolean>
suspend fun leave(): Result<Unit>
@ -337,5 +335,7 @@ interface MatrixRoom : Closeable {
*/
suspend fun sendCallNotificationIfNeeded(): Result<Unit>
suspend fun setSendQueueEnabled(enabled: Boolean)
override fun close() = destroy()
}

View file

@ -83,6 +83,8 @@ interface Timeline : AutoCloseable {
progressCallback: ProgressCallback?
): Result<MediaUploadHandler>
suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result<Boolean>
suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler>
@ -91,9 +93,7 @@ interface Timeline : AutoCloseable {
suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit>
suspend fun cancelSend(transactionId: TransactionId): Result<Unit>
suspend fun cancelSend(transactionId: TransactionId): Result<Boolean>
/**
* Share a location message in the room.

View file

@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.core.EventId
@Immutable
sealed interface LocalEventSendState {
data object NotSentYet : LocalEventSendState
data object Canceled : LocalEventSendState
data class SendingFailed(
val error: String

View file

@ -63,7 +63,7 @@ import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.util.SessionDirectoryNameProvider
import io.element.android.libraries.matrix.impl.util.SessionDirectoryProvider
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
@ -95,9 +95,11 @@ import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.BackupState
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.ClientException
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.PowerLevels
import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
@ -151,7 +153,7 @@ class RustMatrixClient(
sessionDispatcher = sessionDispatcher,
)
private val sessionDirectoryNameProvider = SessionDirectoryNameProvider()
private val sessionDirectoryProvider = SessionDirectoryProvider(sessionStore)
private val isLoggingOut = AtomicBoolean(false)
@ -171,6 +173,7 @@ class RustMatrixClient(
isTokenValid = false,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
sessionPath = existingData.sessionPath,
)
sessionStore.updateData(newData)
Timber.d("Removed session data with token: '...$anonymizedToken'.")
@ -198,6 +201,7 @@ class RustMatrixClient(
isTokenValid = true,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
sessionPath = existingData.sessionPath,
)
sessionStore.updateData(newData)
Timber.d("Saved new session data with token: '...$anonymizedToken'.")
@ -482,7 +486,7 @@ class RustMatrixClient(
override suspend fun clearCache() {
close()
baseDirectory.deleteSessionDirectory(deleteCryptoDb = false)
deleteSessionDirectory(deleteCryptoDb = false)
}
override suspend fun logout(ignoreSdkError: Boolean): String? = doLogout(
@ -512,7 +516,7 @@ class RustMatrixClient(
}
}
close()
baseDirectory.deleteSessionDirectory(deleteCryptoDb = true)
deleteSessionDirectory(deleteCryptoDb = true)
if (removeSession) {
sessionStore.removeSession(sessionId.value)
}
@ -551,11 +555,23 @@ class RustMatrixClient(
}.distinctUntilChanged()
}
override suspend fun setAllSendQueuesEnabled(enabled: Boolean) = withContext(sessionDispatcher) {
Timber.i("setAllSendQueuesEnabled($enabled)")
client.enableAllSendQueues(enabled)
}
override fun sendQueueDisabledFlow(): Flow<RoomId> = mxCallbackFlow {
client.subscribeToSendQueueStatus(object : SendQueueRoomErrorListener {
override fun onError(roomId: String, error: ClientException) {
trySend(RoomId(roomId))
}
})
}.buffer(Channel.UNLIMITED)
private suspend fun File.getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {
val sessionDirectoryName = sessionDirectoryNameProvider.provides(sessionId)
val sessionDirectory = File(this@getCacheSize, sessionDirectoryName)
val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext 0L
if (includeCryptoDb) {
sessionDirectory.getSizeOfFiles()
} else {
@ -571,11 +587,10 @@ class RustMatrixClient(
}
}
private suspend fun File.deleteSessionDirectory(
private suspend fun deleteSessionDirectory(
deleteCryptoDb: Boolean = false,
): Boolean = withContext(sessionDispatcher) {
val sessionDirectoryName = sessionDirectoryNameProvider.provides(sessionId)
val sessionDirectory = File(this@deleteSessionDirectory, sessionDirectoryName)
val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext false
if (deleteCryptoDb) {
// Delete the folder and all its content
sessionDirectory.deleteRecursively()

View file

@ -46,7 +46,7 @@ class RustMatrixClientFactory @Inject constructor(
private val utdTracker: UtdTracker,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val client = getBaseClientBuilder()
val client = getBaseClientBuilder(sessionData.sessionPath)
.homeserverUrl(sessionData.homeserverUrl)
.username(sessionData.userId)
.passphrase(sessionData.passphrase)
@ -71,9 +71,9 @@ class RustMatrixClientFactory @Inject constructor(
)
}
internal fun getBaseClientBuilder(): ClientBuilder {
internal fun getBaseClientBuilder(sessionPath: String): ClientBuilder {
return ClientBuilder()
.basePath(baseDirectory.absolutePath)
.sessionPath(sessionPath)
.userAgent(userAgentProvider.provide())
.addRootCertificates(userCertificatesProvider.provides())
.serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))

View file

@ -1,36 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.OidcConfig
import org.matrix.rustcomponents.sdk.OidcConfiguration
val oidcConfiguration: OidcConfiguration = OidcConfiguration(
clientName = "Element",
redirectUri = OidcConfig.REDIRECT_URI,
clientUri = "https://element.io",
logoUri = "https://element.io/mobile-icon.png",
tosUri = "https://element.io/acceptable-use-policy-terms",
policyUri = "https://element.io/privacy",
contacts = listOf(
"support@element.io",
),
// Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
staticRegistrations = mapOf(
"https://id.thirdroom.io/realms/thirdroom" to "elementx",
),
)

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.OidcConfig
import org.matrix.rustcomponents.sdk.OidcConfiguration
import java.io.File
import javax.inject.Inject
class OidcConfigurationProvider @Inject constructor(
private val baseDirectory: File,
) {
fun get(): OidcConfiguration = OidcConfiguration(
clientName = "Element",
redirectUri = OidcConfig.REDIRECT_URI,
clientUri = "https://element.io",
logoUri = "https://element.io/mobile-icon.png",
tosUri = "https://element.io/acceptable-use-policy-terms",
policyUri = "https://element.io/privacy",
contacts = listOf(
"support@element.io",
),
// Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
staticRegistrations = mapOf(
"https://id.thirdroom.io/realms/thirdroom" to "elementx",
),
dynamicRegistrationsFile = File(baseDirectory, "oidc/registrations.json").absolutePath,
)
}

View file

@ -54,6 +54,7 @@ import org.matrix.rustcomponents.sdk.QrLoginProgressListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
import java.util.UUID
import javax.inject.Inject
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
@ -68,17 +69,19 @@ class RustMatrixAuthenticationService @Inject constructor(
private val passphraseGenerator: PassphraseGenerator,
userCertificatesProvider: UserCertificatesProvider,
proxyProvider: ProxyProvider,
private val oidcConfigurationProvider: OidcConfigurationProvider,
) : MatrixAuthenticationService {
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
// stored in the SessionData.
private val pendingPassphrase = getDatabasePassphrase()
private val sessionPath = File(baseDirectory, UUID.randomUUID().toString()).absolutePath
private val authService: RustAuthenticationService = RustAuthenticationService(
basePath = baseDirectory.absolutePath,
sessionPath = sessionPath,
passphrase = pendingPassphrase,
proxy = proxyProvider.provides(),
userAgent = userAgentProvider.provide(),
additionalRootCertificates = userCertificatesProvider.provides(),
oidcConfiguration = oidcConfiguration,
oidcConfiguration = oidcConfigurationProvider.get(),
customSlidingSyncProxy = null,
sessionDelegate = null,
crossProcessRefreshLockId = null,
@ -148,6 +151,7 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
)
}
sessionStore.storeData(sessionData)
@ -196,6 +200,7 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
)
}
pendingOidcAuthenticationData?.close()
@ -211,11 +216,11 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
runCatching {
val client = rustMatrixClientFactory.getBaseClientBuilder()
val client = rustMatrixClientFactory.getBaseClientBuilder(sessionPath)
.passphrase(pendingPassphrase)
.buildWithQrCode(
qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData,
oidcConfiguration = oidcConfiguration,
oidcConfiguration = oidcConfigurationProvider.get(),
progressListener = object : QrLoginProgressListener {
override fun onUpdate(state: QrLoginProgress) {
Timber.d("QR Code login progress: $state")
@ -229,6 +234,7 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.QR,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
)
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)

View file

@ -25,6 +25,7 @@ internal fun Session.toSessionData(
isTokenValid: Boolean,
loginType: LoginType,
passphrase: String?,
sessionPath: String,
) = SessionData(
userId = userId,
deviceId = deviceId,
@ -37,4 +38,5 @@ internal fun Session.toSessionData(
isTokenValid = isTokenValid,
loginType = loginType,
passphrase = passphrase,
sessionPath = sessionPath,
)

View file

@ -84,6 +84,7 @@ import org.matrix.rustcomponents.sdk.WidgetCapabilities
import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider
import org.matrix.rustcomponents.sdk.getElementCallRequiredPermissions
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk.RoomPowerLevelChanges
import java.io.File
import org.matrix.rustcomponents.sdk.Room as InnerRoom
@ -324,12 +325,6 @@ class RustMatrixRoom(
return liveTimeline.sendMessage(body, htmlBody, mentions)
}
override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) {
runCatching {
innerRoom.redact(eventId.value, reason)
}
}
override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.leave()
@ -435,10 +430,10 @@ class RustMatrixRoom(
}
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> {
return liveTimeline.retrySendMessage(transactionId)
return Result.failure(UnsupportedOperationException("Not supported"))
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> {
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
return liveTimeline.cancelSend(transactionId)
}
@ -600,6 +595,11 @@ class RustMatrixRoom(
innerRoom.sendCallNotificationIfNeeded()
}
override suspend fun setSendQueueEnabled(enabled: Boolean) = withContext(roomDispatcher) {
Timber.d("setSendQueuesEnabled: $enabled")
innerRoom.enableSendQueue(enabled)
}
private fun createTimeline(
timeline: InnerTimeline,
isLive: Boolean,

View file

@ -279,12 +279,32 @@ class RustTimeline(
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(dispatcher) {
messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content ->
runCatching {
runCatching<Unit> {
inner.send(content)
}
}
}
override suspend fun redactEvent(eventId: EventId?, transactionId: TransactionId?, reason: String?): Result<Boolean> = withContext(dispatcher) {
runCatching {
when {
eventId != null -> {
inner.getEventTimelineItemByEventId(eventId.value).use {
inner.redactEvent(item = it, reason = reason)
}
}
transactionId != null -> {
inner.getEventTimelineItemByTransactionId(transactionId.value).use {
inner.redactEvent(item = it, reason = reason)
}
}
else -> {
error("Either eventId or transactionId must be non-null")
}
}
}
}
override suspend fun editMessage(
originalEventId: EventId?,
transactionId: TransactionId?,
@ -293,21 +313,24 @@ class RustTimeline(
mentions: List<Mention>,
): Result<Unit> =
withContext(dispatcher) {
if (originalEventId != null) {
runCatching {
val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value)
editedEvent.use {
inner.edit(
newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()),
editItem = it,
)
runCatching<Unit> {
when {
originalEventId != null -> {
val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value)
editedEvent.use {
inner.edit(
newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()),
editItem = it,
)
}
specialModeEventTimelineItem = null
}
transactionId != null -> {
error("Editing local echo is not supported yet.")
}
else -> {
error("Either originalEventId or transactionId must be non null")
}
specialModeEventTimelineItem = null
}
} else {
runCatching {
transactionId?.let { cancelSend(it) }
inner.send(messageEventContentFromParts(body, htmlBody))
}
}
}
@ -426,17 +449,7 @@ class RustTimeline(
}
}
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = withContext(dispatcher) {
runCatching {
inner.retrySend(transactionId.value)
}
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = withContext(dispatcher) {
runCatching {
inner.cancelSend(transactionId.value)
}
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> = redactEvent(eventId = null, transactionId = transactionId, reason = null)
override suspend fun sendLocation(
body: String,

View file

@ -79,7 +79,6 @@ fun RustEventSendState?.map(): LocalEventSendState? {
RustEventSendState.NotSentYet -> LocalEventSendState.NotSentYet
is RustEventSendState.SendingFailed -> LocalEventSendState.SendingFailed(error)
is RustEventSendState.Sent -> LocalEventSendState.Sent(EventId(eventId))
RustEventSendState.Cancelled -> LocalEventSendState.Canceled
}
}

View file

@ -17,10 +17,15 @@
package io.element.android.libraries.matrix.impl.util
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.SessionStore
import java.io.File
import javax.inject.Inject
class SessionDirectoryNameProvider {
// Rust sanitises the user ID replacing invalid characters with an _
fun provides(sessionId: SessionId): String {
return sessionId.value.replace(":", "_")
class SessionDirectoryProvider @Inject constructor(
private val sessionStore: SessionStore,
) {
suspend fun provides(sessionId: SessionId): File? {
val path = sessionStore.getSession(sessionId.value)?.sessionPath ?: return null
return File(path)
}
}

View file

@ -48,13 +48,16 @@ import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryS
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import java.util.Optional
@ -298,4 +301,13 @@ class FakeMatrixClient(
}
override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId)
var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean ->
// no-op
}
override suspend fun setAllSendQueuesEnabled(enabled: Boolean) = setAllSendQueuesEnabledLambda(enabled)
var sendQueueDisabledFlow = emptyFlow<RoomId>()
override fun sendQueueDisabledFlow(): Flow<RoomId> = sendQueueDisabledFlow
}

View file

@ -113,7 +113,7 @@ class FakeMatrixRoom(
private var updateUserRoleResult = Result.success(Unit)
private var toggleReactionResult = Result.success(Unit)
private var retrySendMessageResult = Result.success(Unit)
private var cancelSendResult = Result.success(Unit)
private var cancelSendResult = Result.success(true)
private var forwardEventResult = Result.success(Unit)
private var reportContentResult = Result.success(Unit)
private var kickUserResult = Result.success(Unit)
@ -282,7 +282,7 @@ class FakeMatrixRoom(
return retrySendMessageResult
}
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> {
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> {
cancelSendCount++
return cancelSendResult
}
@ -295,14 +295,6 @@ class FakeMatrixRoom(
return eventPermalinkResult(eventId)
}
var redactEventEventIdParam: EventId? = null
private set
override suspend fun redactEvent(eventId: EventId, reason: String?): Result<Unit> {
redactEventEventIdParam = eventId
return Result.success(Unit)
}
override suspend fun leave(): Result<Unit> {
return leaveRoomLambda()
}
@ -533,6 +525,9 @@ class FakeMatrixRoom(
return sendCallNotificationIfNeededResult()
}
var setSendQueueEnabledLambda = { _: Boolean -> }
override suspend fun setSendQueueEnabled(enabled: Boolean) = setSendQueueEnabledLambda(enabled)
override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result<MatrixWidgetDriver> = getWidgetDriverResult
fun givenRoomMembersState(state: MatrixRoomMembersState) {
@ -631,7 +626,7 @@ class FakeMatrixRoom(
retrySendMessageResult = result
}
fun givenCancelSendResult(result: Result<Unit>) {
fun givenCancelSendResult(result: Result<Boolean>) {
cancelSendResult = result
}

View file

@ -69,6 +69,16 @@ class FakeTimeline(
mentions: List<Mention>,
): Result<Unit> = sendMessageLambda(body, htmlBody, mentions)
var redactEventLambda: (eventId: EventId?, transactionId: TransactionId?, reason: String?) -> Result<Boolean> = { _, _, _ ->
Result.success(true)
}
override suspend fun redactEvent(
eventId: EventId?,
transactionId: TransactionId?,
reason: String?
): Result<Boolean> = redactEventLambda(eventId, transactionId, reason)
var editMessageLambda: (
originalEventId: EventId?,
transactionId: TransactionId?,
@ -219,11 +229,7 @@ class FakeTimeline(
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
var retrySendMessageLambda: (transactionId: TransactionId) -> Result<Unit> = { Result.success(Unit) }
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = retrySendMessageLambda(transactionId)
var cancelSendLambda: (transactionId: TransactionId) -> Result<Unit> = { Result.success(Unit) }
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = cancelSendLambda(transactionId)
override suspend fun cancelSend(transactionId: TransactionId): Result<Boolean> = redactEvent(null, transactionId, null)
var sendLocationLambda: (
body: String,

View file

@ -63,6 +63,7 @@ dependencies {
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test)
}

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.pushproviders.firebase
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
@ -31,11 +30,10 @@ import io.element.android.libraries.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemoryMultiSessionsStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
@ -165,22 +163,4 @@ class DefaultFirebaseNewTokenHandlerTest {
firebaseStore = firebaseStore
)
}
private fun aSessionData(
sessionId: SessionId,
): SessionData {
return SessionData(
userId = sessionId.value,
deviceId = "aDeviceId",
accessToken = "anAccessToken",
refreshToken = "aRefreshToken",
homeserverUrl = "aHomeserverUrl",
oidcData = null,
slidingSyncProxy = null,
loginTimestamp = null,
isTokenValid = true,
loginType = LoginType.UNKNOWN,
passphrase = null,
)
}
}

View file

@ -44,4 +44,6 @@ data class SessionData(
val loginType: LoginType,
/** The optional passphrase used to encrypt data in the SDK local store. */
val passphrase: String?,
/** The path to the session data stored in the filesystem. */
val sessionPath: String,
)

View file

@ -34,6 +34,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
isTokenValid = if (isTokenValid) 1L else 0L,
loginType = loginType.name,
passphrase = passphrase,
sessionPath = sessionPath,
)
}
@ -50,5 +51,6 @@ internal fun DbSessionData.toApiModel(): SessionData {
isTokenValid = isTokenValid == 1L,
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
passphrase = passphrase,
sessionPath = sessionPath,
)
}

View file

@ -32,7 +32,9 @@ import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider
object SessionStorageModule {
@Provides
@SingleIn(AppScope::class)
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase {
fun provideMatrixDatabase(
@ApplicationContext context: Context,
): SessionDatabase {
val name = "session_database"
val secretFile = context.getDatabasePath("$name.key")

View file

@ -23,7 +23,9 @@ CREATE TABLE SessionData (
isTokenValid INTEGER NOT NULL DEFAULT 1,
loginType TEXT,
-- added in version 5
passphrase TEXT
passphrase TEXT,
-- added in version 6
sessionPath TEXT NOT NULL DEFAULT ""
);

View file

@ -0,0 +1,4 @@
-- Migrate DB from version 7
-- Add sessionPath so we can track the anonymized path for the session files dir
ALTER TABLE SessionData ADD COLUMN sessionPath TEXT NOT NULL DEFAULT "";

View file

@ -144,6 +144,7 @@ class DatabaseSessionStoreTest {
isTokenValid = 1,
loginType = null,
passphrase = "aPassphrase",
sessionPath = "sessionPath",
)
val secondSessionData = SessionData(
userId = "userId",
@ -157,6 +158,7 @@ class DatabaseSessionStoreTest {
isTokenValid = 1,
loginType = null,
passphrase = "aPassphraseAltered",
sessionPath = "sessionPath",
)
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
@ -193,6 +195,7 @@ class DatabaseSessionStoreTest {
isTokenValid = 1,
loginType = null,
passphrase = "aPassphrase",
sessionPath = "sessionPath",
)
val secondSessionData = SessionData(
userId = "userIdUnknown",
@ -206,6 +209,7 @@ class DatabaseSessionStoreTest {
isTokenValid = 1,
loginType = null,
passphrase = "aPassphraseAltered",
sessionPath = "sessionPath",
)
assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)

View file

@ -31,4 +31,5 @@ internal fun aSessionData() = SessionData(
isTokenValid = 1,
loginType = LoginType.UNKNOWN.name,
passphrase = null,
sessionPath = "sessionPath",
)

View file

@ -36,5 +36,6 @@ fun aSessionData(
isTokenValid = isTokenValid,
loginType = LoginType.UNKNOWN,
passphrase = null,
sessionPath = "/a/path/to/a/session",
)
}

View file

@ -30,6 +30,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
import io.element.android.libraries.matrix.impl.auth.OidcConfigurationProvider
import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.sessionstorage.api.LoggedInState
@ -66,6 +67,7 @@ class MainActivity : ComponentActivity() {
passphraseGenerator = NullPassphraseGenerator(),
userCertificatesProvider = userCertificatesProvider,
proxyProvider = proxyProvider,
oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory),
)
}

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94dca1fe2f633ff25724bfc287718f506dee8a18710518fd682de67d9d3d2350
size 14742

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e2588aa42d2fd215f4019e823c8aae87b9ca44b5164b382cc6015f726945168c
size 13478

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e924c2fc2dc6b66146364d999b77874c883fae2abac9390a1a48c90f61ebf3f7
size 4928
oid sha256:de9876d82b13a220cd296f6117bedf7c1a260d9560978701065ce068e22b1c8b
size 4485

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a4dd7d2d6607135bb9e18f684a34df17e0ef1fd2b48fb85bbc6ba82b82e09c5
size 6213
oid sha256:cce57f0b6d304c9871fdb3f8a13e1dd787493d95d5edde9631a18e45b9756acc
size 5704

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ee2232b7d0feb4c7310a695f3b1e58ddb466970a8cef4051e35f7d83dbb1629
size 4863
oid sha256:5dc95cafaeda3d0cd45ce37408de0858543f1de07a29b18d165cf2c9a438e9c6
size 4465

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bfa1209e3c4c97dffe0f633643a8e4da3509d57998cb047651a7e18daeddb566
size 6080
oid sha256:e7d670f258934a3b9b016464f74739a0eff902ec9a184bfb166b116e01de36da
size 5698

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f5503888d07219df789c5394553491ad4fe142e7f806d92d2aad95d626d343bd
size 30501
oid sha256:29cceba22078f6b7791bd5785c7654b68114a7858d043aa5e3b71764089c8ab7
size 31482

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:45309834e68ab0b6c6efbb5bfe26cc61f2b9fc171df8734df69dd9c9b2fd9b06
size 34219
oid sha256:00e60c9996b9f76848eaa6178aa50ecc4aedd880536800bd307fb68db9c17011
size 35177

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:724c3407a1b62b22549081165b40faaf26d151e8901209f58dc09465586e6c3c
size 30971
oid sha256:c3bb375b7a5e3e1566c73693cb19e9a38a33b9f40148ffd219a2e0c2037d7a97
size 32137

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1675d944150065d33f6b375ae7bc617385099a4bcb76e55a213f322146788d0f
size 34402
oid sha256:57c3265ca6432efdc69ed45acdb719a10a72544c520b32008c203294bd92f18d
size 35571

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2a90813041638a500f77ea67b7d2bd5eda67340aa3167694dd056b3d3b335170
size 49858
oid sha256:af29bab33aa3acd1cf0141d339c54764b76ef79978519ad33078dadb2bab39b5
size 50347

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7234d292e54eda42bce2d3ebf5077d663c18e15f5d4fd40d8fedd05696fcab7
size 70828
oid sha256:d45f5492470427b578e9a89147114df46b8e97669aa821356ffcb1b13669c360
size 71317

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:28b52a5faf15fed991aff80f2dc7f4da19d3a8eb5f274d880debfd99f502b3d5
size 328963
oid sha256:90d6e29da0c6e203b5337496aef692f579f3754c3709735c98d650fe4e519b5b
size 310747

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a8cd185f8149044a4254d418ff5ee670ad85c8d8f2ce235a15045d170c02b8f5
size 85699
oid sha256:b3534ec458fa98111819b74453086b203dbd69cbc9aee70141203444f56b01f2
size 84090

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7aa599537bff4a9db1cf532eab36fc20c823cebb86fded64b53d9084693467b
size 51322
oid sha256:fa1157e50a67d9fbb25dcb2fba467a684709a329ed2dc6aca9178ccd66e654b7
size 51816

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:566646a9a50e33618141ad6c36291b6e4b620f36d7735723adcca6adf9fbf935
size 63060
oid sha256:66ef24a752440232d199c490b3d75db9f9348b5f9665e1f3a668f9496634fd34
size 63525

Some files were not shown because too many files have changed in this diff Show more