Merge branch 'develop' into feature/fga/room_list_api
This commit is contained in:
commit
7ee3c1bf42
114 changed files with 2437 additions and 170 deletions
|
|
@ -16,18 +16,28 @@
|
|||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.androidutils"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.anvilannotations)
|
||||
implementation(projects.libraries.di)
|
||||
|
||||
implementation(projects.libraries.core)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.activity.activity)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(projects.libraries.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.androidutils.clipboard
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class AndroidClipboardHelper @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ClipboardHelper {
|
||||
|
||||
private val clipboardManager = requireNotNull(context.getSystemService<ClipboardManager>())
|
||||
|
||||
override fun copyPlainText(text: String) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("", text))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.androidutils.clipboard
|
||||
|
||||
/**
|
||||
* Wrapper class for handling clipboard operations so it can be used in JVM environments.
|
||||
*/
|
||||
interface ClipboardHelper {
|
||||
fun copyPlainText(text: String)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.androidutils.clipboard
|
||||
|
||||
class FakeClipboardHelper : ClipboardHelper {
|
||||
|
||||
var clipboardContents: Any? = null
|
||||
|
||||
override fun copyPlainText(text: String) {
|
||||
clipboardContents = text
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.theme.components
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
|
|
@ -52,6 +53,7 @@ fun ModalBottomSheet(
|
|||
tonalElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
androidx.compose.material3.ModalBottomSheet(
|
||||
|
|
@ -64,6 +66,7 @@ fun ModalBottomSheet(
|
|||
tonalElevation = tonalElevation,
|
||||
scrimColor = scrimColor,
|
||||
dragHandle = dragHandle,
|
||||
windowInsets = windowInsets,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,14 @@ import androidx.compose.material3.SnackbarHostState
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -52,19 +53,16 @@ class SnackbarDispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
/** Used to provide a [SnackbarDispatcher] to composable functions, it's needed for [rememberSnackbarHostState]. */
|
||||
val LocalSnackbarDispatcher = compositionLocalOf<SnackbarDispatcher> {
|
||||
error("No SnackbarDispatcher provided")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun handleSnackbarMessage(
|
||||
snackbarDispatcher: SnackbarDispatcher
|
||||
): SnackbarMessage? {
|
||||
val snackbarMessage by snackbarDispatcher.snackbarMessage.collectAsState(initial = null)
|
||||
LaunchedEffect(snackbarMessage) {
|
||||
if (snackbarMessage != null) {
|
||||
launch {
|
||||
snackbarDispatcher.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
return snackbarMessage
|
||||
return snackbarDispatcher.snackbarMessage.collectAsState(initial = null).value
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -74,6 +72,7 @@ fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostSt
|
|||
val snackbarMessageText = snackbarMessage?.let {
|
||||
stringResource(id = snackbarMessage.messageResId)
|
||||
}
|
||||
val dispatcher = LocalSnackbarDispatcher.current
|
||||
LaunchedEffect(snackbarMessage) {
|
||||
if (snackbarMessageText == null) return@LaunchedEffect
|
||||
coroutineScope.launch {
|
||||
|
|
@ -81,6 +80,9 @@ fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostSt
|
|||
message = snackbarMessageText,
|
||||
duration = snackbarMessage.duration,
|
||||
)
|
||||
if (isActive) {
|
||||
dispatcher.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
return snackbarHostState
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
class ForwardEventException(
|
||||
val roomIds: List<RoomId>
|
||||
) : Exception() {
|
||||
|
||||
override val message: String? = "Failed to deliver event to $roomIds rooms"
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo
|
|||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.Closeable
|
||||
|
|
@ -86,6 +87,8 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun sendReaction(emoji: String, eventId: EventId): Result<Unit>
|
||||
|
||||
suspend fun forwardEvent(eventId: EventId, rooms: List<RoomId>): Result<Unit>
|
||||
|
||||
suspend fun retrySendMessage(transactionId: String): Result<Unit>
|
||||
|
||||
suspend fun cancelSend(transactionId: String): Result<Unit>
|
||||
|
|
@ -111,4 +114,6 @@ interface MatrixRoom : Closeable {
|
|||
suspend fun setName(name: String): Result<Unit>
|
||||
|
||||
suspend fun setTopic(topic: String): Result<Unit>
|
||||
|
||||
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
|
||||
sealed interface EventSendState {
|
||||
object NotSentYet : EventSendState
|
||||
object Canceled : EventSendState
|
||||
|
||||
data class SendingFailed(
|
||||
val error: String
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
|||
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
||||
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
|
||||
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
|
||||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
||||
import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource
|
||||
import io.element.android.libraries.matrix.impl.room.roomOrNull
|
||||
|
|
@ -79,7 +80,7 @@ class RustMatrixClient constructor(
|
|||
|
||||
override val sessionId: UserId = UserId(client.userId())
|
||||
|
||||
private val roomListService = client.roomList()
|
||||
private val roomListService = client.roomListService()
|
||||
private val sessionCoroutineScope = childScopeOf(appCoroutineScope, dispatchers.main, "Session-${sessionId}")
|
||||
private val verificationService = RustSessionVerificationService()
|
||||
private val syncService = RustSyncService(roomListService, sessionCoroutineScope)
|
||||
|
|
@ -112,6 +113,8 @@ class RustMatrixClient constructor(
|
|||
|
||||
private val roomMembershipObserver = RoomMembershipObserver()
|
||||
|
||||
private val roomContentForwarder = RoomContentForwarder(roomListService)
|
||||
|
||||
init {
|
||||
client.setDelegate(clientDelegate)
|
||||
syncService.syncState
|
||||
|
|
@ -132,7 +135,8 @@ class RustMatrixClient constructor(
|
|||
innerRoom = fullRoom,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
coroutineDispatchers = dispatchers,
|
||||
systemClock = clock
|
||||
systemClock = clock,
|
||||
roomContentForwarder = roomContentForwarder,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class RustNotificationService(
|
|||
eventId: EventId
|
||||
): Result<NotificationData?> {
|
||||
return runCatching {
|
||||
client.getNotificationItem(roomId.value, eventId.value).use(notificationMapper::map)
|
||||
client.getNotificationItem(roomId.value, eventId.value)?.use(notificationMapper::map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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.room
|
||||
|
||||
import io.element.android.libraries.core.coroutine.parallelMap
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.ForwardEventException
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.SlidingSync
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Helper to forward event contents from a room to a set of other rooms.
|
||||
* @param slidingSync the [SlidingSync] to fetch room instances to forward the event to
|
||||
*/
|
||||
class RoomContentForwarder(
|
||||
private val roomListService: RoomListService,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Forwards the event with the given [eventId] from the [fromRoom] to the given [toRoomIds].
|
||||
* @param fromRoom the room to forward the event from
|
||||
* @param eventId the id of the event to forward
|
||||
* @param toRoomIds the ids of the rooms to forward the event to
|
||||
* @param timeoutMs the maximum time in milliseconds to wait for the event to be sent to a room
|
||||
*/
|
||||
suspend fun forward(
|
||||
fromRoom: Room,
|
||||
eventId: EventId,
|
||||
toRoomIds: List<RoomId>,
|
||||
timeoutMs: Long = 5000L
|
||||
) {
|
||||
val content = fromRoom.getTimelineEventContentByEventId(eventId.value)
|
||||
val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) }
|
||||
val targetRooms = targetSlidingSyncRooms.mapNotNull { slidingSyncRoom -> slidingSyncRoom.use { it.fullRoom() } }
|
||||
val failedForwardingTo = mutableSetOf<RoomId>()
|
||||
targetRooms.parallelMap { room ->
|
||||
room.use { targetRoom ->
|
||||
val result = runCatching {
|
||||
// Sending a message requires a registered timeline listener
|
||||
targetRoom.addTimelineListener(NoOpTimelineListener)
|
||||
withTimeout(timeoutMs.milliseconds) {
|
||||
targetRoom.send(content, genTransactionId())
|
||||
}
|
||||
}
|
||||
// After sending, we remove the timeline
|
||||
targetRoom.removeTimeline()
|
||||
result
|
||||
}.onFailure {
|
||||
failedForwardingTo.add(RoomId(room.id()))
|
||||
if (it is CancellationException) {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failedForwardingTo.isNotEmpty()) {
|
||||
throw ForwardEventException(toRoomIds.toList())
|
||||
}
|
||||
}
|
||||
|
||||
private object NoOpTimelineListener : TimelineListener {
|
||||
override fun onUpdate(diff: TimelineDiff) = Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -3,40 +3,31 @@ package io.element.android.libraries.matrix.impl.room
|
|||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.matrix.rustcomponents.sdk.RoomList
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||
import org.matrix.rustcomponents.sdk.RoomListInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomListState
|
||||
import org.matrix.rustcomponents.sdk.RoomListStateListener
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncListLoadingState
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncListStateObserver
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingState
|
||||
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceState
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
|
||||
import timber.log.Timber
|
||||
|
||||
fun RoomListInterface.roomListStateFlow(): Flow<RoomListState> =
|
||||
fun RoomList.loadingStateFlow(): Flow<RoomListLoadingState> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListStateListener {
|
||||
override fun onUpdate(state: RoomListState) {
|
||||
val listener = object : RoomListLoadingStateListener {
|
||||
override fun onUpdate(state: RoomListLoadingState) {
|
||||
trySendBlocking(state)
|
||||
}
|
||||
}
|
||||
state(listener)
|
||||
val result = loadingState(listener)
|
||||
send(result.state)
|
||||
result.stateStream
|
||||
}
|
||||
|
||||
fun RoomListInterface.loadingStateFlow(): Flow<SlidingSyncListLoadingState> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : SlidingSyncListStateObserver {
|
||||
override fun didReceiveUpdate(newState: SlidingSyncListLoadingState) {
|
||||
trySendBlocking(newState)
|
||||
}
|
||||
}
|
||||
val result = entriesLoadingState(listener)
|
||||
send(result.entriesLoadingState)
|
||||
result.entriesLoadingStateStream
|
||||
}
|
||||
|
||||
fun RoomListInterface.roomListEntriesUpdateFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<RoomListEntriesUpdate> =
|
||||
fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit): Flow<RoomListEntriesUpdate> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListEntriesListener {
|
||||
override fun onUpdate(roomEntriesUpdate: RoomListEntriesUpdate) {
|
||||
|
|
@ -48,7 +39,7 @@ fun RoomListInterface.roomListEntriesUpdateFlow(onInitialList: suspend (List<Roo
|
|||
result.entriesStream
|
||||
}
|
||||
|
||||
fun RoomListInterface.roomOrNull(roomId: String): RoomListItem? {
|
||||
fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
|
||||
return try {
|
||||
room(roomId)
|
||||
} catch (failure: Throwable) {
|
||||
|
|
@ -56,3 +47,13 @@ fun RoomListInterface.roomOrNull(roomId: String): RoomListItem? {
|
|||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun RoomListService.stateFlow(): Flow<RoomListServiceState> =
|
||||
mxCallbackFlow {
|
||||
val listener = object : RoomListServiceStateListener {
|
||||
override fun onUpdate(state: RoomListServiceState) {
|
||||
trySendBlocking(state)
|
||||
}
|
||||
}
|
||||
state(listener)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import org.matrix.rustcomponents.sdk.RoomMember
|
|||
import org.matrix.rustcomponents.sdk.RoomSubscription
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
class RustMatrixRoom(
|
||||
|
|
@ -64,6 +65,7 @@ class RustMatrixRoom(
|
|||
sessionCoroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val systemClock: SystemClock,
|
||||
private val roomContentForwarder: RoomContentForwarder,
|
||||
) : MatrixRoom {
|
||||
|
||||
override val roomId = RoomId(innerRoom.id())
|
||||
|
|
@ -307,6 +309,14 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
roomContentForwarder.forward(fromRoom = innerRoom, eventId = eventId, toRoomIds = roomIds)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun retrySendMessage(transactionId: String): Result<Unit> =
|
||||
withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
|
|
@ -350,9 +360,19 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun fetchMembers() = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.fetchMembers()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)
|
||||
if (blockUserId != null) {
|
||||
innerRoom.ignoreUser(blockUserId.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
|||
import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||
import org.matrix.rustcomponents.sdk.RoomListException
|
||||
import org.matrix.rustcomponents.sdk.RoomListInput
|
||||
import org.matrix.rustcomponents.sdk.RoomListInterface
|
||||
import org.matrix.rustcomponents.sdk.RoomListRange
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
internal class RustRoomSummaryDataSource(
|
||||
private val roomListService: RoomListInterface,
|
||||
private val roomListService: RoomListService,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
|
|
@ -48,7 +48,7 @@ internal class RustRoomSummaryDataSource(
|
|||
|
||||
fun init() {
|
||||
sessionCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
roomListService.roomListEntriesUpdateFlow { roomListEntries ->
|
||||
roomListService.allRooms().entriesFlow { roomListEntries ->
|
||||
roomList.value = roomListEntries.map(::buildSummaryForRoomListEntry)
|
||||
}.onEach { update ->
|
||||
roomList.getAndUpdate {
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@
|
|||
package io.element.android.libraries.matrix.impl.sync
|
||||
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import org.matrix.rustcomponents.sdk.RoomListState
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceState
|
||||
|
||||
internal fun RoomListState.toSyncState(): SyncState {
|
||||
internal fun RoomListServiceState.toSyncState(): SyncState {
|
||||
return when (this) {
|
||||
RoomListState.INIT,
|
||||
RoomListState.SETTING_UP -> SyncState.Idle
|
||||
RoomListState.RUNNING -> SyncState.Syncing
|
||||
RoomListState.ERROR -> SyncState.InError
|
||||
RoomListState.TERMINATED -> SyncState.Terminated
|
||||
RoomListServiceState.INIT,
|
||||
RoomListServiceState.SETTING_UP -> SyncState.Idle
|
||||
RoomListServiceState.RUNNING -> SyncState.Syncing
|
||||
RoomListServiceState.ERROR -> SyncState.InError
|
||||
RoomListServiceState.TERMINATED -> SyncState.Terminated
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,20 +18,18 @@ package io.element.android.libraries.matrix.impl.sync
|
|||
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.impl.room.roomListStateFlow
|
||||
import io.element.android.libraries.matrix.impl.room.stateFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.matrix.rustcomponents.sdk.RoomList
|
||||
import org.matrix.rustcomponents.sdk.RoomListState
|
||||
import timber.log.Timber
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.RoomListServiceState
|
||||
|
||||
class RustSyncService(
|
||||
private val roomListService: RoomList,
|
||||
private val roomListService: RoomListService,
|
||||
private val sessionCoroutineScope: CoroutineScope
|
||||
) : SyncService {
|
||||
|
||||
|
|
@ -49,8 +47,8 @@ class RustSyncService(
|
|||
|
||||
override val syncState: StateFlow<SyncState> =
|
||||
roomListService
|
||||
.roomListStateFlow()
|
||||
.map(RoomListState::toSyncState)
|
||||
.stateFlow()
|
||||
.map(RoomListServiceState::toSyncState)
|
||||
.distinctUntilChanged()
|
||||
.stateIn(sessionCoroutineScope, SharingStarted.WhileSubscribed(), SyncState.Idle)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ fun RustEventSendState?.map(): EventSendState? {
|
|||
RustEventSendState.NotSentYet -> EventSendState.NotSentYet
|
||||
is RustEventSendState.SendingFailed -> EventSendState.SendingFailed(error)
|
||||
is RustEventSendState.Sent -> EventSendState.Sent(EventId(eventId))
|
||||
RustEventSendState.Cancelled -> EventSendState.Canceled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ class FakeMatrixRoom(
|
|||
private var sendReactionResult = Result.success(Unit)
|
||||
private var retrySendMessageResult = Result.success(Unit)
|
||||
private var cancelSendResult = Result.success(Unit)
|
||||
private var forwardEventResult = Result.success(Unit)
|
||||
private var reportContentResult = Result.success(Unit)
|
||||
|
||||
var sendMediaCount = 0
|
||||
private set
|
||||
|
|
@ -88,6 +90,9 @@ class FakeMatrixRoom(
|
|||
var cancelSendCount: Int = 0
|
||||
private set
|
||||
|
||||
var reportedContentCount: Int = 0
|
||||
private set
|
||||
|
||||
var isInviteAccepted: Boolean = false
|
||||
private set
|
||||
|
||||
|
|
@ -218,6 +223,10 @@ class FakeMatrixRoom(
|
|||
|
||||
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia()
|
||||
|
||||
override suspend fun forwardEvent(eventId: EventId, rooms: List<RoomId>): Result<Unit> = simulateLongTask {
|
||||
forwardEventResult
|
||||
}
|
||||
|
||||
private suspend fun fakeSendMedia(): Result<Unit> = simulateLongTask {
|
||||
sendMediaResult.onSuccess {
|
||||
sendMediaCount++
|
||||
|
|
@ -244,6 +253,15 @@ class FakeMatrixRoom(
|
|||
setTopicResult
|
||||
}
|
||||
|
||||
override suspend fun reportContent(
|
||||
eventId: EventId,
|
||||
reason: String,
|
||||
blockUserId: UserId?
|
||||
): Result<Unit> = simulateLongTask {
|
||||
reportedContentCount++
|
||||
return reportContentResult
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
|
||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||
|
|
@ -329,4 +347,12 @@ class FakeMatrixRoom(
|
|||
fun givenCancelSendResult(result: Result<Unit>) {
|
||||
cancelSendResult = result
|
||||
}
|
||||
|
||||
fun givenForwardEventResult(result: Result<Unit>) {
|
||||
forwardEventResult = result
|
||||
}
|
||||
|
||||
fun givenReportContentResult(result: Result<Unit>) {
|
||||
reportContentResult = result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun SelectedRoom(
|
||||
roomSummary: RoomSummaryDetails,
|
||||
modifier: Modifier = Modifier,
|
||||
onRoomRemoved: (RoomSummaryDetails) -> Unit = {},
|
||||
) {
|
||||
Box(modifier = modifier
|
||||
.width(56.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Avatar(AvatarData(roomSummary.roomId.value, roomSummary.name, roomSummary.avatarURLString, AvatarSize.Custom(56.dp)))
|
||||
Text(
|
||||
text = roomSummary.name,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(20.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
.clickable(
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onRoomRemoved(roomSummary) }
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(id = StringR.string.action_remove),
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.padding(2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SelectedRoomLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SelectedRoomDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SelectedRoom(roomSummary =
|
||||
RoomSummaryDetails(
|
||||
roomId = RoomId("!room:domain"),
|
||||
name = "roomName",
|
||||
canonicalAlias = null,
|
||||
isDirect = true,
|
||||
avatarURLString = null,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
inviter = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
<string name="action_invite">"Invite"</string>
|
||||
<string name="action_invite_friends">"Invite friends"</string>
|
||||
<string name="action_invite_friends_to_app">"Invite friends to %1$s"</string>
|
||||
<string name="action_invite_people_to_app">"Invite people to %1$s"</string>
|
||||
<string name="action_invites_list">"Invites"</string>
|
||||
<string name="action_learn_more">"Learn more"</string>
|
||||
<string name="action_leave">"Leave"</string>
|
||||
|
|
@ -108,6 +109,7 @@
|
|||
<string name="common_server_not_supported">"Server not supported"</string>
|
||||
<string name="common_server_url">"Server URL"</string>
|
||||
<string name="common_settings">"Settings"</string>
|
||||
<string name="common_shared_location">"Shared location"</string>
|
||||
<string name="common_starting_chat">"Starting chat…"</string>
|
||||
<string name="common_sticker">"Sticker"</string>
|
||||
<string name="common_success">"Success"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue