diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index fdad01d83c..db3d3dfe2d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -20,4 +20,5 @@ sealed interface RoomDetailsEvent { data object LeaveRoom : RoomDetailsEvent data object MuteNotification : RoomDetailsEvent data object UnmuteNotification : RoomDetailsEvent + data class SetIsFavorite(val isFavorite: Boolean) : RoomDetailsEvent } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index a232d4e87e..4b6e33b288 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -44,9 +44,11 @@ import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -95,6 +97,7 @@ class RoomDetailsPresenter @Inject constructor( val dmMember by room.getDirectRoomMember(membersState) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomType by getRoomType(dmMember) + val isFavorite by isFavorite() val topicState = remember(canEditTopic, roomTopic, roomType) { val topic = roomTopic @@ -122,6 +125,12 @@ class RoomDetailsPresenter @Inject constructor( client.notificationSettingsService().unmuteRoom(room.roomId, room.isEncrypted, room.isOneToOne) } } + is RoomDetailsEvent.SetIsFavorite -> { + scope.launch(dispatchers.io) { + val tags = RoomNotableTags(isFavorite = event.isFavorite) + room.updateNotableTags(tags) + } + } } } @@ -142,6 +151,7 @@ class RoomDetailsPresenter @Inject constructor( roomMemberDetailsState = roomMemberDetailsState, leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), + isFavorite = isFavorite, eventSink = ::handleEvents, ) } @@ -164,6 +174,11 @@ class RoomDetailsPresenter @Inject constructor( } } + @Composable + private fun isFavorite() = remember { + room.notableTagsFlow.map { it.isFavorite } + }.collectAsState(initial = false) + @Composable private fun getCanInvite(membersState: MatrixRoomMembersState) = produceState(false, membersState) { value = room.canInvite().getOrElse { false } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index 8dc6f81bf1..5ba39af5bf 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -36,6 +36,7 @@ data class RoomDetailsState( val canShowNotificationSettings: Boolean, val leaveRoomState: LeaveRoomState, val roomNotificationSettings: RoomNotificationSettings?, + val isFavorite: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index b84302463f..2493efc855 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -36,6 +36,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider aDmRoomDetailsState().copy(roomName = "Daniel"), aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"), aRoomDetailsState().copy(canInvite = true), + aRoomDetailsState().copy(isFavorite = true), aRoomDetailsState().copy( canEdit = true, // Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed @@ -86,6 +87,7 @@ fun aRoomDetailsState() = RoomDetailsState( roomMemberDetailsState = null, leaveRoomState = aLeaveRoomState(), roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false), + isFavorite = false, eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index bbdccbc302..401f6d024a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -61,6 +61,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.MainActionButton import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -163,6 +164,13 @@ fun RoomDetailsView( ) } + FavoriteSection( + isFavorite = state.isFavorite, + onFavoriteChanges = { + state.eventSink(RoomDetailsEvent.SetIsFavorite(it)) + } + ) + if (state.roomType is RoomDetailsType.Room) { MembersSection( memberCount = state.memberCount, @@ -355,6 +363,22 @@ private fun NotificationSection( } } +@Composable +private fun FavoriteSection( + isFavorite: Boolean, + onFavoriteChanges: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + PreferenceCategory(modifier = modifier) { + PreferenceSwitch( + icon = CompoundIcons.FavouriteOff, + title = "Favorite", + isChecked = isFavorite, + onCheckedChange = onFavoriteChanges + ) + } +} + @Composable private fun MembersSection( memberCount: Long, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt index 1d2cf2c910..f5ce9897b4 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -56,7 +58,10 @@ fun RoomListContextMenu( onLeaveRoomClicked = { eventSink(RoomListEvents.HideContextMenu) eventSink(RoomListEvents.LeaveRoom(contextMenu.roomId)) - } + }, + onFavoriteChanged = { isFavorite -> + eventSink(RoomListEvents.MarkRoomAsFavorite(contextMenu.roomId, isFavorite)) + }, ) } } @@ -66,6 +71,7 @@ private fun RoomListModalBottomSheetContent( contextMenu: RoomListState.ContextMenu.Shown, onRoomSettingsClicked: (roomId: RoomId) -> Unit, onLeaveRoomClicked: (roomId: RoomId) -> Unit, + onFavoriteChanged: (isFavorite: Boolean) -> Unit, ) { Column( modifier = Modifier.fillMaxWidth() @@ -78,6 +84,31 @@ private fun RoomListModalBottomSheetContent( ) } ) + ListItem( + headlineContent = { + Text( + text = "Favourite", + style = MaterialTheme.typography.bodyLarge, + ) + }, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector( + CompoundIcons.FavouriteOff, + contentDescription = "Favourite" + ) + ), + trailingContent = ListItemContent.Switch( + checked = contextMenu.isFavorite.dataOrNull().orFalse(), + enabled = contextMenu.isFavorite is AsyncData.Success, + onChange = { isFavorite -> + onFavoriteChanged(isFavorite) + }, + ), + onClick = { + onFavoriteChanged(!contextMenu.isFavorite.dataOrNull().orFalse()) + }, + style = ListItemStyle.Primary, + ) ListItem( headlineContent = { Text( @@ -96,11 +127,13 @@ private fun RoomListModalBottomSheetContent( ) ListItem( headlineContent = { - val leaveText = stringResource(id = if (contextMenu.isDm) { - CommonStrings.action_leave_conversation - } else { - CommonStrings.action_leave_room - }) + val leaveText = stringResource( + id = if (contextMenu.isDm) { + CommonStrings.action_leave_conversation + } else { + CommonStrings.action_leave_room + } + ) Text(text = leaveText) }, modifier = Modifier.clickable { onLeaveRoomClicked(contextMenu.roomId) }, @@ -126,9 +159,11 @@ internal fun RoomListModalBottomSheetContentPreview() = ElementPreview { roomId = RoomId(value = "!aRoom:aDomain"), roomName = "aRoom", isDm = false, + isFavorite = AsyncData.Success(false), ), onRoomSettingsClicked = {}, - onLeaveRoomClicked = {} + onLeaveRoomClicked = {}, + onFavoriteChanged = {}, ) } @@ -140,8 +175,10 @@ internal fun RoomListModalBottomSheetContentForDmPreview() = ElementPreview { roomId = RoomId(value = "!aRoom:aDomain"), roomName = "aRoom", isDm = true, + isFavorite = AsyncData.Success(false), ), onRoomSettingsClicked = {}, - onLeaveRoomClicked = {} + onLeaveRoomClicked = {}, + onFavoriteChanged = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index d54bc604cf..b804db5315 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -28,4 +28,5 @@ sealed interface RoomListEvents { data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents data object HideContextMenu : RoomListEvents data class LeaveRoom(val roomId: RoomId) : RoomListEvents + data class MarkRoomAsFavorite(val roomId: RoomId, val isFavorite: Boolean) : RoomListEvents } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 8ab1bde3f4..595f75c2fa 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.leaveroom.api.LeaveRoomEvent @@ -32,7 +33,10 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.coroutine.cancel +import io.element.android.libraries.architecture.coroutine.rememberJob import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -41,11 +45,17 @@ import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.getCurrentUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject private const val EXTENDED_RANGE_SIZE = 40 @@ -72,12 +82,12 @@ class RoomListPresenter @Inject constructor( val filteredRoomList by roomListDataSource.filteredRooms.collectAsState() val filter by roomListDataSource.filter.collectAsState() val networkConnectionStatus by networkMonitor.connectivity.collectAsState() + val coroutineScope = rememberCoroutineScope() LaunchedEffect(Unit) { roomListDataSource.launchIn(this) initialLoad(matrixUser) } - // Session verification status (unknown, not verified, verified) val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) } @@ -101,8 +111,8 @@ class RoomListPresenter @Inject constructor( val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() var displaySearchResults by rememberSaveable { mutableStateOf(false) } - - var contextMenu by remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } + val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } + val showContextMenuJob = rememberJob() fun handleEvents(event: RoomListEvents) { when (event) { @@ -117,14 +127,14 @@ class RoomListPresenter @Inject constructor( displaySearchResults = !displaySearchResults } is RoomListEvents.ShowContextMenu -> { - contextMenu = RoomListState.ContextMenu.Shown( - roomId = event.roomListRoomSummary.roomId, - roomName = event.roomListRoomSummary.name, - isDm = event.roomListRoomSummary.isDm, - ) + showContextMenuJob.value = coroutineScope.showContextMenu(event, contextMenu) + } + is RoomListEvents.HideContextMenu -> { + showContextMenuJob.cancel() + contextMenu.value = RoomListState.ContextMenu.Hidden } - is RoomListEvents.HideContextMenu -> contextMenu = RoomListState.ContextMenu.Hidden is RoomListEvents.LeaveRoom -> leaveRoomState.eventSink(LeaveRoomEvent.ShowConfirmation(event.roomId)) + is RoomListEvents.MarkRoomAsFavorite -> coroutineScope.markRoomAsFavorite(event) } } @@ -142,7 +152,7 @@ class RoomListPresenter @Inject constructor( hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, invitesState = inviteStateDataSource.inviteState(), displaySearchResults = displaySearchResults, - contextMenu = contextMenu, + contextMenu = contextMenu.value, leaveRoomState = leaveRoomState, eventSink = ::handleEvents ) @@ -152,6 +162,38 @@ class RoomListPresenter @Inject constructor( matrixUser.value = client.getCurrentUser() } + private fun CoroutineScope.showContextMenu(event: RoomListEvents.ShowContextMenu, contextMenuState: MutableState) = launch { + val initialState = RoomListState.ContextMenu.Shown( + roomId = event.roomListRoomSummary.roomId, + roomName = event.roomListRoomSummary.name, + isDm = event.roomListRoomSummary.isDm, + isFavorite = AsyncData.Loading(), + ) + contextMenuState.value = initialState + val room = client.getRoom(event.roomListRoomSummary.roomId) + if (room != null) { + room.notableTagsFlow + .distinctUntilChanged() + .onEach { tags -> + val newState = initialState.copy(isFavorite = AsyncData.Success(tags.isFavorite)) + contextMenuState.value = newState + } + .launchIn(this) + } else { + contextMenuState.value = initialState.copy(isFavorite = AsyncData.Failure(IllegalStateException("Room not found"))) + } + } + + private fun CoroutineScope.markRoomAsFavorite(event: RoomListEvents.MarkRoomAsFavorite) = launch { + val room = client.getRoom(event.roomId) + if (room != null) { + val notableTags = RoomNotableTags(isFavorite = event.isFavorite); + room.updateNotableTags(notableTags) + }else { + Timber.w("Room ${event.roomId} not found, can't mark as favorite"); + } + } + private fun updateVisibleRange(range: IntRange) { if (range.isEmpty()) return val midExtendedRangeSize = EXTENDED_RANGE_SIZE / 2 @@ -162,3 +204,5 @@ class RoomListPresenter @Inject constructor( client.roomListService.updateAllRoomsVisibleRange(extendedRange) } } + + diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 7003da36e5..5f5a5fda4c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -47,6 +48,7 @@ data class RoomListState( val roomId: RoomId, val roomName: String, val isDm: Boolean, + val isFavorite: AsyncData, ) : ContextMenu } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index b1e43f147e..7ac34668c7 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -46,6 +47,7 @@ open class RoomListStateProvider : PreviewParameterProvider { roomId = RoomId("!aRoom:aDomain"), roomName = "A nice room name", isDm = false, + isFavorite = AsyncData.Success(true), ) ), aRoomListState().copy(displayRecoveryKeyPrompt = true), diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/coroutine/Job.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/coroutine/Job.kt new file mode 100644 index 0000000000..bf3d4f783e --- /dev/null +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/coroutine/Job.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.architecture.coroutine + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import kotlinx.coroutines.Job +import kotlin.coroutines.cancellation.CancellationException + +@Composable +fun rememberJob(): MutableState = remember { + mutableStateOf(null) +} + +fun MutableState.cancel(cause: CancellationException? = null) { + value?.cancel(cause) + value = null +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 550bb7012a..630f87a0fb 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -29,6 +29,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.poll.PollKind import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags 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.MatrixWidgetSettings @@ -57,6 +58,8 @@ interface MatrixRoom : Closeable { val roomInfoFlow: Flow + val notableTagsFlow: Flow + /** * 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). @@ -150,6 +153,8 @@ interface MatrixRoom : Closeable { suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result + suspend fun updateNotableTags(notableTags: RoomNotableTags): Result + /** * Share a location message in the room. * diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tags/RoomNotableTags.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tags/RoomNotableTags.kt new file mode 100644 index 0000000000..25a19b5697 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/tags/RoomNotableTags.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.room.tags + +data class RoomNotableTags( + val isFavorite: Boolean, +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 3fc7d65468..9c0cd11e3f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags 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.MatrixWidgetSettings @@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.impl.notificationsettings.RustNotific 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.member.RoomMemberListFetcher +import io.element.android.libraries.matrix.impl.room.tags.map import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver @@ -70,6 +72,7 @@ import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation +import org.matrix.rustcomponents.sdk.RoomNotableTagsListener import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider @@ -79,6 +82,7 @@ import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File import org.matrix.rustcomponents.sdk.Room as InnerRoom +import uniffi.matrix_sdk_base.RoomNotableTags as RustRoomNotableTags import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @OptIn(ExperimentalCoroutinesApi::class) @@ -111,6 +115,15 @@ class RustMatrixRoom( }) } + override val notableTagsFlow: Flow = mxCallbackFlow { + innerRoom.subscribeToNotableTags(object : RoomNotableTagsListener { + override fun call(notableTags: RustRoomNotableTags) { + Timber.d("On notable tags update: $notableTags") + channel.trySend(notableTags.map()) + } + }) + } + // Create a dispatcher for all room methods... private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) @@ -423,6 +436,17 @@ class RustMatrixRoom( } } + override suspend fun updateNotableTags(notableTags: RoomNotableTags): Result = withContext(roomDispatcher) { + runCatching { + Timber.i("Update notable tags with : $notableTags") + innerRoom.updateNotableTags(notableTags.map()) + }.onFailure { + Timber.w("Failed to update notable tags: $it") + }.onSuccess { + Timber.i("Successfully updated notable tags") + } + } + override suspend fun sendLocation( body: String, geoUri: String, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tags/RoomNotableTagsMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tags/RoomNotableTagsMapper.kt new file mode 100644 index 0000000000..36fe0eb98e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/tags/RoomNotableTagsMapper.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.room.tags + +import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags +import uniffi.matrix_sdk_base.RoomNotableTags as RustRoomNotableTags + +fun RustRoomNotableTags.map() = RoomNotableTags( + isFavorite = isFavorite +) + +fun RoomNotableTags.map() = RustRoomNotableTags( + isFavorite = isFavorite +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt index 1b3e900f17..4934b0058b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryDetailsFactory.kt @@ -25,6 +25,7 @@ import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.use class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) { + fun create(roomInfo: RoomInfo): RoomSummaryDetails { val latestRoomMessage = roomInfo.latestEvent?.use { roomMessageFactory.create(it) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index ed56d4a8d0..20cd781dcb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSende import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import kotlinx.collections.immutable.ImmutableList +import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import org.matrix.rustcomponents.sdk.Reaction @@ -36,7 +37,6 @@ import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails import org.matrix.rustcomponents.sdk.Receipt as RustReceipt -import uniffi.matrix_sdk_ui.EventItemOrigin as RustEventItemOrigin class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper()) { fun map(eventTimelineItem: RustEventTimelineItem): EventTimelineItem = eventTimelineItem.use { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index c33a6d43b2..72508440ea 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -40,6 +40,7 @@ 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.location.AssetType +import io.element.android.libraries.matrix.api.room.tags.RoomNotableTags 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 @@ -113,6 +114,7 @@ class FakeMatrixRoom( private var getWidgetDriverResult: Result = Result.success(FakeWidgetDriver()) private var canUserTriggerRoomNotificationResult: Result = Result.success(true) private var canUserJoinCallResult: Result = Result.success(true) + private var updateNotableTagsResult = Result.success(Unit) var sendMessageMentions = emptyList() val editMessageCalls = mutableListOf>() private val _typingRecord = mutableListOf() @@ -169,6 +171,9 @@ class FakeMatrixRoom( private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) override val roomInfoFlow: Flow = _roomInfoFlow + private val _notableTagsFlow: MutableSharedFlow = MutableStateFlow(aRoomNotableTags()) + override val notableTagsFlow: Flow = _notableTagsFlow + override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override val roomNotificationSettingsStateFlow: MutableStateFlow = @@ -374,6 +379,14 @@ class FakeMatrixRoom( return reportContentResult } + override suspend fun updateNotableTags(notableTags: RoomNotableTags): Result { + return updateNotableTagsResult.also { result -> + if (result.isSuccess) { + _notableTagsFlow.emit(notableTags) + } + } + } + override suspend fun sendLocation( body: String, geoUri: String, @@ -571,6 +584,10 @@ class FakeMatrixRoom( getWidgetDriverResult = result } + fun givenUpdateNotableTagsResult(result: Result) { + updateNotableTagsResult = result + } + fun givenRoomInfo(roomInfo: MatrixRoomInfo) { _roomInfoFlow.tryEmit(roomInfo) } @@ -610,6 +627,7 @@ fun aRoomInfo( isPublic: Boolean = true, isSpace: Boolean = false, isTombstoned: Boolean = false, + isFavorite: Boolean = false, canonicalAlias: String? = null, alternativeAliases: List = emptyList(), currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, @@ -646,3 +664,9 @@ fun aRoomInfo( hasRoomCall = hasRoomCall, activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(), ) + +fun aRoomNotableTags( + isFavorite: Boolean = false, +) = RoomNotableTags( + isFavorite = isFavorite, +)