Update the chat screen UI using RoomInfo. (#1640)

* Update the chat screen UI using `RoomInfo`.

This is specially useful for getting live values for `hasRoomCall`.

* Ensure the first `MatrixRoomInfo` is emitted ASAP

* Try excluding `*Present$present$*` inner functions from kover as separate entities

* Update strings

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-10-26 16:27:42 +02:00 committed by GitHub
parent 4d9a3bddaf
commit c40a39bbfa
36 changed files with 338 additions and 52 deletions

View file

@ -200,6 +200,7 @@ koverMerged {
"*Node$*", "*Node$*",
// Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix SDK api, so it is not really relevant to unit test it: there is no logic to test. // Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix SDK api, so it is not really relevant to unit test it: there is no logic to test.
"io.element.android.libraries.matrix.impl.*", "io.element.android.libraries.matrix.impl.*",
"*Presenter\$present\$*"
) )
) )
} }
@ -252,6 +253,7 @@ koverMerged {
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
// Some options can't be tested at the moment // Some options can't be tested at the moment
excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*"
excludes += "*Presenter\$present\$*"
} }
bound { bound {
minValue = 85 minValue = 85

1
changelog.d/1158.feature Normal file
View file

@ -0,0 +1 @@
Element Call: change the 'join call' button in a chat room when there's an active call.

View file

@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -74,6 +75,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState 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.MessageEventType
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
@ -113,6 +115,7 @@ class MessagesPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): MessagesState { override fun present(): MessagesState {
val roomInfo by room.roomInfoFlow.collectAsState(null)
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val composerState = composerPresenter.present() val composerState = composerPresenter.present()
val voiceMessageComposerState = voiceMessageComposerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present()
@ -125,14 +128,13 @@ class MessagesPresenter @AssistedInject constructor(
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value) val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
var roomName: Async<String> by remember { mutableStateOf(Async.Uninitialized) } val roomName: Async<String> by remember {
var roomAvatar: Async<AvatarData> by remember { mutableStateOf(Async.Uninitialized) } derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized }
LaunchedEffect(syncUpdateFlow.value) {
withContext(dispatchers.io) {
roomName = Async.Success(room.displayName)
roomAvatar = Async.Success(room.avatarData())
}
} }
val roomAvatar: Async<AvatarData> by remember {
derivedStateOf { roomInfo?.avatarData()?.let { Async.Success(it) } ?: Async.Uninitialized }
}
var hasDismissedInviteDialog by rememberSaveable { var hasDismissedInviteDialog by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
@ -207,14 +209,15 @@ class MessagesPresenter @AssistedInject constructor(
enableVoiceMessages = enableVoiceMessages, enableVoiceMessages = enableVoiceMessages,
enableInRoomCalls = enableInRoomCalls, enableInRoomCalls = enableInRoomCalls,
appName = buildMeta.applicationName, appName = buildMeta.applicationName,
isCallOngoing = roomInfo?.hasRoomCall ?: false,
eventSink = { handleEvents(it) } eventSink = { handleEvents(it) }
) )
} }
private fun MatrixRoom.avatarData(): AvatarData { private fun MatrixRoomInfo.avatarData(): AvatarData {
return AvatarData( return AvatarData(
id = roomId.value, id = id,
name = displayName, name = name,
url = avatarUrl, url = avatarUrl,
size = AvatarSize.TimelineRoom size = AvatarSize.TimelineRoom
) )

View file

@ -50,6 +50,7 @@ data class MessagesState(
val enableTextFormatting: Boolean, val enableTextFormatting: Boolean,
val enableVoiceMessages: Boolean, val enableVoiceMessages: Boolean,
val enableInRoomCalls: Boolean, val enableInRoomCalls: Boolean,
val isCallOngoing: Boolean,
val appName: String, val appName: String,
val eventSink: (MessagesEvents) -> Unit val eventSink: (MessagesEvents) -> Unit
) )

View file

@ -63,6 +63,9 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
attachmentsState = AttachmentsState.Sending.Uploading(0.33f) attachmentsState = AttachmentsState.Sending.Uploading(0.33f)
), ),
), ),
aMessagesState().copy(
isCallOngoing = true,
)
) )
} }
@ -102,6 +105,7 @@ fun aMessagesState() = MessagesState(
enableTextFormatting = true, enableTextFormatting = true,
enableVoiceMessages = true, enableVoiceMessages = true,
enableInRoomCalls = true, enableInRoomCalls = true,
isCallOngoing = false,
appName = "Element", appName = "Element",
eventSink = {} eventSink = {}
) )

View file

@ -21,17 +21,21 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -94,6 +98,7 @@ import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import timber.log.Timber import timber.log.Timber
import androidx.compose.material3.Button as Material3Button
@Composable @Composable
fun MessagesView( fun MessagesView(
@ -174,6 +179,7 @@ fun MessagesView(
inRoomCallsEnabled = state.enableInRoomCalls, inRoomCallsEnabled = state.enableInRoomCalls,
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked, onRoomDetailsClicked = onRoomDetailsClicked,
isCallOngoing = state.isCallOngoing,
onJoinCallClicked = onJoinCallClicked, onJoinCallClicked = onJoinCallClicked,
) )
} }
@ -375,6 +381,7 @@ private fun MessagesViewTopBar(
roomName: String?, roomName: String?,
roomAvatar: AvatarData?, roomAvatar: AvatarData?,
inRoomCallsEnabled: Boolean, inRoomCallsEnabled: Boolean,
isCallOngoing: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onRoomDetailsClicked: () -> Unit = {}, onRoomDetailsClicked: () -> Unit = {},
onJoinCallClicked: () -> Unit = {}, onJoinCallClicked: () -> Unit = {},
@ -402,15 +409,48 @@ private fun MessagesViewTopBar(
}, },
actions = { actions = {
if (inRoomCallsEnabled) { if (inRoomCallsEnabled) {
IconButton(onClick = onJoinCallClicked) { if (isCallOngoing) {
Icon(CommonDrawables.ic_compound_video_call, contentDescription = null) // TODO add proper content description once we have the state JoinCallMenuItem(onJoinCallClicked = onJoinCallClicked)
} else {
IconButton(onClick = onJoinCallClicked) {
Icon(CommonDrawables.ic_compound_video_call, contentDescription = stringResource(CommonStrings.a11y_start_call))
}
} }
} }
Spacer(Modifier.width(8.dp))
}, },
windowInsets = WindowInsets(0.dp) windowInsets = WindowInsets(0.dp)
) )
} }
@Composable
private fun JoinCallMenuItem(
modifier: Modifier = Modifier,
onJoinCallClicked: () -> Unit,
) {
Material3Button(
onClick = onJoinCallClicked,
colors = ButtonDefaults.buttonColors(
contentColor = ElementTheme.colors.bgCanvasDefault,
containerColor = ElementTheme.colors.iconAccentTertiary
),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
modifier = modifier.heightIn(min = 36.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
resourceId = CommonDrawables.ic_compound_video_call,
contentDescription = null
)
Spacer(Modifier.width(8.dp))
Text(
text = stringResource(CommonStrings.action_join),
style = ElementTheme.typography.fontBodyMdMedium
)
Spacer(Modifier.width(8.dp))
}
}
@Composable @Composable
private fun RoomAvatarAndNameRow( private fun RoomAvatarAndNameRow(
roomName: String, roomName: String,

View file

@ -68,6 +68,7 @@ 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_SESSION_ID_2
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaSender
@ -106,7 +107,8 @@ class MessagesPresenterTest {
val initialState = consumeItemsUntilTimeout().last() val initialState = consumeItemsUntilTimeout().last()
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID) assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
assertThat(initialState.roomName).isEqualTo(Async.Success("")) assertThat(initialState.roomName).isEqualTo(Async.Success(""))
assertThat(initialState.roomAvatar).isEqualTo(Async.Success(AvatarData(id = A_ROOM_ID.value, name = "", size = AvatarSize.TimelineRoom))) assertThat(initialState.roomAvatar)
.isEqualTo(Async.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)))
assertThat(initialState.userHasPermissionToSendMessage).isTrue() assertThat(initialState.userHasPermissionToSendMessage).isTrue()
assertThat(initialState.userHasPermissionToRedact).isFalse() assertThat(initialState.userHasPermissionToRedact).isFalse()
assertThat(initialState.hasNetworkConnection).isTrue() assertThat(initialState.hasNetworkConnection).isTrue()
@ -603,7 +605,9 @@ class MessagesPresenterTest {
private fun TestScope.createMessagesPresenter( private fun TestScope.createMessagesPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom(), matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(id = roomId.value, name = ""))
},
navigator: FakeMessagesNavigator = FakeMessagesNavigator(), navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(),

View file

@ -0,0 +1,21 @@
/*
* 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
enum class CurrentUserMembership {
INVITED, JOINED, LEFT
}

View file

@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
@ -51,6 +52,8 @@ interface MatrixRoom : Closeable {
val activeMemberCount: Long val activeMemberCount: Long
val joinedMemberCount: Long val joinedMemberCount: Long
val roomInfoFlow: Flow<MatrixRoomInfo>
/** /**
* A one-to-one is a room with exactly 2 members. * A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).

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.api.room
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
data class MatrixRoomInfo(
val id: String,
val name: String?,
val topic: String?,
val avatarUrl: String?,
val isDirect: Boolean,
val isPublic: Boolean,
val isSpace: Boolean,
val isTombstoned: Boolean,
val canonicalAlias: String?,
val alternativeAliases: List<String>,
val currentUserMembership: CurrentUserMembership,
val latestEvent: EventTimelineItem?,
val inviter: RoomMember?,
val activeMembersCount: Long,
val invitedMembersCount: Long,
val joinedMembersCount: Long,
val highlightCount: Long,
val notificationCount: Long,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val activeRoomCallParticipants: List<String>
)

View file

@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.impl.notification.RustNotificationSer
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.oidc.toRustAction import io.element.android.libraries.matrix.impl.oidc.toRustAction
import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
@ -201,7 +202,8 @@ class RustMatrixClient constructor(
systemClock = clock, systemClock = clock,
roomContentForwarder = roomContentForwarder, roomContentForwarder = roomContentForwarder,
sessionData = sessionStore.getSession(sessionId.value)!!, sessionData = sessionStore.getSession(sessionId.value)!!,
roomSyncSubscriber = roomSyncSubscriber roomSyncSubscriber = roomSyncSubscriber,
matrixRoomInfoMapper = MatrixRoomInfoMapper(),
) )
} }
} }

View file

@ -0,0 +1,69 @@
/*
* 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.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.Membership as RustMembership
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
class MatrixRoomInfoMapper(
private val timelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(),
) {
fun map(rustRoomInfo: RustRoomInfo): MatrixRoomInfo = rustRoomInfo.use {
return MatrixRoomInfo(
id = it.id,
name = it.name,
topic = it.topic,
avatarUrl = it.avatarUrl,
isDirect = it.isDirect,
isPublic = it.isPublic,
isSpace = it.isSpace,
isTombstoned = it.isTombstoned,
canonicalAlias = it.canonicalAlias,
alternativeAliases = it.alternativeAliases,
currentUserMembership = it.membership.map(),
latestEvent = it.latestEvent?.use (timelineItemMapper::map),
inviter = it.inviter?.use(RoomMemberMapper::map),
activeMembersCount = it.activeMembersCount.toLong(),
invitedMembersCount = it.invitedMembersCount.toLong(),
joinedMembersCount = it.joinedMembersCount.toLong(),
highlightCount = it.highlightCount.toLong(),
notificationCount = it.notificationCount.toLong(),
userDefinedNotificationMode = it.userDefinedNotificationMode?.map(),
hasRoomCall = it.hasRoomCall,
activeRoomCallParticipants = it.activeRoomCallParticipants
)
}
}
fun RustMembership.map(): CurrentUserMembership = when(this) {
RustMembership.INVITED -> CurrentUserMembership.INVITED
RustMembership.JOINED -> CurrentUserMembership.JOINED
RustMembership.LEFT -> CurrentUserMembership.LEFT
}
fun RustRoomNotificationMode.map(): RoomNotificationMode = when(this) {
RustRoomNotificationMode.ALL_MESSAGES -> RoomNotificationMode.ALL_MESSAGES
RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE
}

View file

@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.impl.poll.toInner
import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.util.destroyAll import io.element.android.libraries.matrix.impl.util.destroyAll
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl
import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionData
@ -59,12 +61,16 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.EventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
@ -73,6 +79,7 @@ import org.matrix.rustcomponents.sdk.WidgetCapabilities
import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -88,10 +95,23 @@ class RustMatrixRoom(
private val roomContentForwarder: RoomContentForwarder, private val roomContentForwarder: RoomContentForwarder,
private val sessionData: SessionData, private val sessionData: SessionData,
private val roomSyncSubscriber: RoomSyncSubscriber, private val roomSyncSubscriber: RoomSyncSubscriber,
private val matrixRoomInfoMapper: MatrixRoomInfoMapper,
) : MatrixRoom { ) : MatrixRoom {
override val roomId = RoomId(innerRoom.id()) override val roomId = RoomId(innerRoom.id())
override val roomInfoFlow: Flow<MatrixRoomInfo> = mxCallbackFlow {
launch {
val initial = innerRoom.roomInfo().use(matrixRoomInfoMapper::map)
channel.trySend(initial)
}
innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener {
override fun call(roomInfo: RoomInfo) {
channel.trySend(matrixRoomInfoMapper.map(roomInfo))
}
})
}
// Create a dispatcher for all room methods... // Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)

View file

@ -68,7 +68,11 @@ class RustWidgetDriver(
} }
override suspend fun send(message: String) { override suspend fun send(message: String) {
driverAndHandle.handle.send(message) try {
driverAndHandle.handle.send(message)
} catch (e: IllegalStateException) {
// The handle is closed, ignore
}
} }
override fun close() { override fun close() {

View file

@ -29,16 +29,23 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
@ -46,6 +53,8 @@ import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.io.File import java.io.File
@ -143,6 +152,9 @@ class FakeMatrixRoom(
private var leaveRoomError: Throwable? = null private var leaveRoomError: Throwable? = null
private val _roomInfoFlow: MutableSharedFlow<MatrixRoomInfo> = MutableStateFlow(aRoomInfo())
override val roomInfoFlow: Flow<MatrixRoomInfo> = _roomInfoFlow
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown) override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> = override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =
@ -497,6 +509,10 @@ class FakeMatrixRoom(
fun givenGetWidgetDriverResult(result: Result<MatrixWidgetDriver>) { fun givenGetWidgetDriverResult(result: Result<MatrixWidgetDriver>) {
getWidgetDriverResult = result getWidgetDriverResult = result
} }
fun givenRoomInfo(roomInfo: MatrixRoomInfo) {
_roomInfoFlow.tryEmit(roomInfo)
}
} }
data class SendLocationInvocation( data class SendLocationInvocation(
@ -523,3 +539,49 @@ data class EndPollInvocation(
val pollStartId: EventId, val pollStartId: EventId,
val text: String, val text: String,
) )
fun aRoomInfo(
id: String = A_ROOM_ID.value,
name: String? = A_ROOM_NAME,
topic: String? = "A topic",
avatarUrl: String? = AN_AVATAR_URL,
isDirect: Boolean = false,
isPublic: Boolean = true,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
canonicalAlias: String? = null,
alternativeAliases: List<String> = emptyList(),
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
latestEvent: EventTimelineItem? = null,
inviter: RoomMember? = null,
activeMembersCount: Long = 1,
invitedMembersCount: Long = 0,
joinedMembersCount: Long = 1,
highlightCount: Long = 0,
notificationCount: Long = 0,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
activeRoomCallParticipants: List<String> = emptyList()
) = MatrixRoomInfo(
id = id,
name = name,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases,
currentUserMembership = currentUserMembership,
latestEvent = latestEvent,
inviter = inviter,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
highlightCount = highlightCount,
notificationCount = notificationCount,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
activeRoomCallParticipants = activeRoomCallParticipants
)

View file

@ -11,6 +11,7 @@
<string name="a11y_poll_end">"Ended poll"</string> <string name="a11y_poll_end">"Ended poll"</string>
<string name="a11y_send_files">"Send files"</string> <string name="a11y_send_files">"Send files"</string>
<string name="a11y_show_password">"Show password"</string> <string name="a11y_show_password">"Show password"</string>
<string name="a11y_start_call">"Start a call"</string>
<string name="a11y_user_menu">"User menu"</string> <string name="a11y_user_menu">"User menu"</string>
<string name="a11y_voice_message_record">"Record voice message. Double tap and hold to record. Release to end recording."</string> <string name="a11y_voice_message_record">"Record voice message. Double tap and hold to record. Release to end recording."</string>
<string name="action_accept">"Accept"</string> <string name="action_accept">"Accept"</string>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5f219bd9b9363f237e15bb73655dd53b2ec143e18c8544c11efbfa90390e091c oid sha256:0eee7139a7675a08899accf49a97139b56333737e649e51ca1644ae6ae68d915
size 54312 size 54358

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6065f5330e1b3638719c6743db098bf6576fcffc6ccf8215f7f600a0b981144b oid sha256:39e0b212a2cc423f3e33254d4a965c3071e88e5dd21268c42b3b555b1800b236
size 55731 size 55774

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f4bdafdfa50665f05ba5cd8749252e7e5d65bea8a8c6bfc1c46eb1acd1570b52 oid sha256:42445e54ca2cfadbbd3f041f327825ea5595e27877376a760c37b746529d0980
size 56086 size 56135

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2bfcc4dcaa8980cfc9c724e64a93116ac1fb81a1bf4421532cb196d4e07db7c5 oid sha256:b9f1300da0ac7ef29286fb37d4489b38fa07632f71aa7d1f1cc0e0786749c626
size 56024 size 56124

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c86a6da3d45b767a41f65e0cf4e8cacefb33e0409386a6233140891deb2258f6 oid sha256:95f81914a56679419a7a6055040ae51b11616c87012310cbd221e3af41d55e8e
size 51965 size 52039

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5a1fc20759ff45eeef547621853b9684a663fe9dcdf78b316454dcae26d078f9 oid sha256:ce97ccc29b01824ddd251dbe7756aa80749c0acc4fd8278cb1c3e4fc24c2337e
size 52286 size 52334

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7b65df67b1384aaa169f25d81e87072f2732c88676122e58e5d08850ada050ce oid sha256:6d387f9678367ec0b8d6617a3ba378d309979af0e534e9f683805e71cd6c2794
size 60073 size 60172

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:abe449182a4cddbf11b9004579d27043e41c74d45261c1435d18b516946f9eed oid sha256:0708776dd8884bd8975845c9fbb0b30fe6317d5692aaba36781fed5cff065c1b
size 42175 size 42279

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5deb0929efaab3160a7d0441bde9afe278064e8c0641cc62408bb25acbdced21 oid sha256:766086d64f70b2a9f028149190d3f3c5b2ff34599907b38997657b6d093fa0f4
size 41329 size 41428

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7b9bc4654b6911d7f50b4b0242b650a529cb22c13a58caf5eae50366a2d27b37 oid sha256:6d42806b454f15ed83e883bf5e2765d345163a57049b4b598c1b8d08458588dc
size 52532 size 52624

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0e051668beb9030ff184d6e75831a00202854f6448880f399a7ee2d5be187740 oid sha256:27f4fd957e004579b8b20373e1649f8c067a604595a5dcaae60b0caec7c43954
size 53880 size 53972

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c6c6603452db218811f3f5a2baa934ca640755b969e81ca8cbf6b3dcf663aee9 oid sha256:397f061ceeefb2ea67a5d9a342050cea136554e936854bd26b92c3acfde45d74
size 54551 size 54646

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e953bc91e9959f1635d12416f6afde79427aeac4edef44fb38f1512672e544bd oid sha256:a1a0280d6321b754e5e36d25eefc2d5c7192af16e52ae1304b4e7c57a7824e19
size 51552 size 51611

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0157713ae934c0771a5d3d4d064ce6dc9be11b1d8011f2360740d7f4a20f6f04 oid sha256:99ded72c99dc1ba5b8c61cbacca6cc91f4b6325a16c12013150c6d63afd28182
size 50162 size 50273

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:24f1d30bb62ed620671e87062a338fd53f409bce6e08fb7b116249decf74e409 oid sha256:d2fc04a6c21e851b4e10c88ad9485f43f2f1576f5aeb40d4611513352341536e
size 50295 size 50384

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9457cbf3ff35a5677f983eba7d5087ca82e4d99a2bb942f64f5f7ccdb71e01b5 oid sha256:5f3a764349e13ebe7205ff4d0b04a02f64c6eee5d65bb3b9cc62ac1d721d2d39
size 54882 size 54944

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:549892647be61e9fd5fbbb80fa14734533e1b4628f95d409f2772dd990000f42 oid sha256:459029e7871910e085378a40ff503d286b72fb7029c251d1e04a8a1f39cfd0ba
size 38953 size 39017

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c6bef454d8f57ba76b34b8588ba73635719feb6b1dde967b90f812d2410a7749 oid sha256:a2d6cb8081da15f265d35e909c1d22bb6a48219162b20e1472230a698fdc2d2b
size 38165 size 38231