UX cleanup: room details (#2816)

* UX cleanup: room details screen

Add new CTA buttons for Invite and Call actions

* Update screenshots

* Fix maestro

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa
2024-05-08 11:42:33 +02:00
committed by GitHub
parent 0f60299de0
commit b524645b89
34 changed files with 149 additions and 94 deletions

View File

@@ -16,7 +16,7 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Create" - tapOn: "Create"
- takeScreenshot: build/maestro/320-createAndDeleteRoom - takeScreenshot: build/maestro/320-createAndDeleteRoom
- tapOn: "aRoomName" - tapOn: "aRoomName"
- tapOn: "Invite people" - tapOn: "Invite"
# assert there's 1 member and 1 invitee # assert there's 1 member and 1 invitee
- tapOn: "Search for someone" - tapOn: "Search for someone"
- inputText: ${MAESTRO_INVITEE2_MXID} - inputText: ${MAESTRO_INVITEE2_MXID}

1
changelog.d/2814.misc Normal file
View File

@@ -0,0 +1 @@
UX cleanup: room details screen, add new CTA buttons for Invite and Call actions.

View File

@@ -86,6 +86,7 @@ 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
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canCall
import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState
import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
@@ -158,9 +159,7 @@ class MessagesPresenter @AssistedInject constructor(
mutableStateOf(false) mutableStateOf(false)
} }
var canJoinCall by rememberSaveable { val canJoinCall by room.canCall(updateKey = syncUpdateFlow.value)
mutableStateOf(false)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Remove the unread flag on entering but don't send read receipts // Remove the unread flag on entering but don't send read receipts
@@ -170,12 +169,6 @@ class MessagesPresenter @AssistedInject constructor(
} }
} }
LaunchedEffect(syncUpdateFlow.value) {
withContext(dispatchers.io) {
canJoinCall = room.canUserJoinCall(room.sessionId).getOrDefault(false)
}
}
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) } val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) } var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow.value) { LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow.value) {

View File

@@ -288,7 +288,7 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent()))
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }
@@ -298,10 +298,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(2) val initialState = awaitFirstItem()
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(initialState.actionListState.target).isEqualTo(ActionListState.Target.None)
// Otherwise we would have some extra items here // Otherwise we would have some extra items here
ensureAllEventsConsumed() ensureAllEventsConsumed()
} }
@@ -335,7 +334,7 @@ class MessagesPresenterTest {
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
assertThat(replyMode.attachmentThumbnailInfo).isNotNull() assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }
@@ -368,7 +367,7 @@ class MessagesPresenterTest {
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
assertThat(replyMode.attachmentThumbnailInfo).isNotNull() assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }
@@ -394,7 +393,7 @@ class MessagesPresenterTest {
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
assertThat(replyMode.attachmentThumbnailInfo).isNotNull() assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }
@@ -408,7 +407,7 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent()))
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java) assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }
@@ -732,7 +731,7 @@ class MessagesPresenterTest {
assertThat(replyMode.attachmentThumbnailInfo).isNotNull() assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
assertThat(replyMode.attachmentThumbnailInfo?.textContent) assertThat(replyMode.attachmentThumbnailInfo?.textContent)
.isEqualTo("What type of food should we have at the party?") .isEqualTo("What type of food should we have at the party?")
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }

View File

@@ -56,8 +56,9 @@ dependencies {
api(projects.libraries.usersearch.api) api(projects.libraries.usersearch.api)
api(projects.services.apperror.api) api(projects.services.apperror.api)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(projects.features.leaveroom.api) implementation(projects.features.call)
implementation(projects.features.createroom.api) implementation(projects.features.createroom.api)
implementation(projects.features.leaveroom.api)
implementation(projects.features.userprofile.shared) implementation(projects.features.userprofile.shared)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.features.poll.api) implementation(projects.features.poll.api)

View File

@@ -16,6 +16,7 @@
package io.element.android.features.roomdetails.impl package io.element.android.features.roomdetails.impl
import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -28,6 +29,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.call.CallType
import io.element.android.features.call.ui.ElementCallActivity
import io.element.android.features.poll.api.history.PollHistoryEntryPoint import io.element.android.features.poll.api.history.PollHistoryEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
@@ -42,10 +45,12 @@ import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.mediaviewer.api.local.MediaInfo import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -54,7 +59,9 @@ import kotlinx.parcelize.Parcelize
class RoomDetailsFlowNode @AssistedInject constructor( class RoomDetailsFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
@ApplicationContext private val context: Context,
private val pollHistoryEntryPoint: PollHistoryEntryPoint, private val pollHistoryEntryPoint: PollHistoryEntryPoint,
private val room: MatrixRoom,
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>( ) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(), initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
@@ -129,6 +136,14 @@ class RoomDetailsFlowNode @AssistedInject constructor(
override fun openAdminSettings() { override fun openAdminSettings() {
backstack.push(NavTarget.AdminSettings) backstack.push(NavTarget.AdminSettings)
} }
override fun onJoinCall() {
val inputs = CallType.RoomCall(
sessionId = room.sessionId,
roomId = room.roomId,
)
ElementCallActivity.start(context, inputs)
}
} }
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback)) createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
} }

View File

@@ -58,6 +58,7 @@ class RoomDetailsNode @AssistedInject constructor(
fun openAvatarPreview(name: String, url: String) fun openAvatarPreview(name: String, url: String)
fun openPollHistory() fun openPollHistory()
fun openAdminSettings() fun openAdminSettings()
fun onJoinCall()
} }
private val callbacks = plugins<Callback>() private val callbacks = plugins<Callback>()
@@ -86,6 +87,10 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.openPollHistory() } callbacks.forEach { it.openPollHistory() }
} }
private fun onJoinCall() {
callbacks.forEach { it.onJoinCall() }
}
private fun CoroutineScope.onShareRoom(context: Context) = launch { private fun CoroutineScope.onShareRoom(context: Context) = launch {
room.getPermalink() room.getPermalink()
.onSuccess { permalink -> .onSuccess { permalink ->
@@ -162,6 +167,7 @@ class RoomDetailsNode @AssistedInject constructor(
openAvatarPreview = ::openAvatarPreview, openAvatarPreview = ::openAvatarPreview,
openPollHistory = ::openPollHistory, openPollHistory = ::openPollHistory,
openAdminSettings = this::openAdminSettings, openAdminSettings = this::openAdminSettings,
onJoinCallClicked = ::onJoinCall,
) )
} }
} }

View File

@@ -44,6 +44,7 @@ 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.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState 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.roomNotificationSettings
import io.element.android.libraries.matrix.ui.room.canCall
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@@ -86,11 +87,14 @@ class RoomDetailsPresenter @Inject constructor(
} }
} }
val syncUpdateTimestamp by room.syncUpdateFlow.collectAsState()
val membersState by room.membersStateFlow.collectAsState() val membersState by room.membersStateFlow.collectAsState()
val canInvite by getCanInvite(membersState) val canInvite by getCanInvite(membersState)
val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME) val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME)
val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR) val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR)
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC) val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
val canJoinCall by room.canCall(updateKey = syncUpdateTimestamp)
val dmMember by room.getDirectRoomMember(membersState) val dmMember by room.getDirectRoomMember(membersState)
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
val roomType by getRoomType(dmMember) val roomType by getRoomType(dmMember)
@@ -138,6 +142,7 @@ class RoomDetailsPresenter @Inject constructor(
canInvite = canInvite, canInvite = canInvite,
canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room, canEdit = (canEditAvatar || canEditName || canEditTopic) && roomType == RoomDetailsType.Room,
canShowNotificationSettings = canShowNotificationSettings.value, canShowNotificationSettings = canShowNotificationSettings.value,
canCall = canJoinCall,
roomType = roomType, roomType = roomType,
roomMemberDetailsState = roomMemberDetailsState, roomMemberDetailsState = roomMemberDetailsState,
leaveRoomState = leaveRoomState, leaveRoomState = leaveRoomState,

View File

@@ -36,6 +36,7 @@ data class RoomDetailsState(
val canEdit: Boolean, val canEdit: Boolean,
val canInvite: Boolean, val canInvite: Boolean,
val canShowNotificationSettings: Boolean, val canShowNotificationSettings: Boolean,
val canCall: Boolean,
val leaveRoomState: LeaveRoomState, val leaveRoomState: LeaveRoomState,
val roomNotificationSettings: RoomNotificationSettings?, val roomNotificationSettings: RoomNotificationSettings?,
val isFavorite: Boolean, val isFavorite: Boolean,

View File

@@ -46,6 +46,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
// Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed // Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed
roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true) roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true)
), ),
aRoomDetailsState(canCall = false, canInvite = false),
// Add other state here // Add other state here
) )
} }
@@ -89,6 +90,7 @@ fun aRoomDetailsState(
canInvite: Boolean = false, canInvite: Boolean = false,
canEdit: Boolean = false, canEdit: Boolean = false,
canShowNotificationSettings: Boolean = true, canShowNotificationSettings: Boolean = true,
canCall: Boolean = true,
roomType: RoomDetailsType = RoomDetailsType.Room, roomType: RoomDetailsType = RoomDetailsType.Room,
roomMemberDetailsState: UserProfileState? = null, roomMemberDetailsState: UserProfileState? = null,
leaveRoomState: LeaveRoomState = aLeaveRoomState(), leaveRoomState: LeaveRoomState = aLeaveRoomState(),
@@ -107,6 +109,7 @@ fun aRoomDetailsState(
canInvite = canInvite, canInvite = canInvite,
canEdit = canEdit, canEdit = canEdit,
canShowNotificationSettings = canShowNotificationSettings, canShowNotificationSettings = canShowNotificationSettings,
canCall = canCall,
roomType = roomType, roomType = roomType,
roomMemberDetailsState = roomMemberDetailsState, roomMemberDetailsState = roomMemberDetailsState,
leaveRoomState = leaveRoomState, leaveRoomState = leaveRoomState,

View File

@@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -100,6 +99,7 @@ fun RoomDetailsView(
openAvatarPreview: (name: String, url: String) -> Unit, openAvatarPreview: (name: String, url: String) -> Unit,
openPollHistory: () -> Unit, openPollHistory: () -> Unit,
openAdminSettings: () -> Unit, openAdminSettings: () -> Unit,
onJoinCallClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun onShareMember() { fun onShareMember() {
@@ -137,7 +137,9 @@ fun RoomDetailsView(
) )
MainActionsSection( MainActionsSection(
state = state, state = state,
onShareRoom = onShareRoom onShareRoom = onShareRoom,
onInvitePeople = invitePeople,
onCall = onJoinCallClicked,
) )
} }
@@ -188,20 +190,12 @@ fun RoomDetailsView(
} }
val displayMemberListItem = state.roomType is RoomDetailsType.Room val displayMemberListItem = state.roomType is RoomDetailsType.Room
val displayInviteMembersItem = state.canInvite if (displayMemberListItem) {
if (displayMemberListItem || displayInviteMembersItem) {
PreferenceCategory { PreferenceCategory {
if (displayMemberListItem) { MembersItem(
MembersItem( memberCount = state.memberCount,
memberCount = state.memberCount, openRoomMemberList = openRoomMemberList,
openRoomMemberList = openRoomMemberList, )
)
}
if (displayInviteMembersItem) {
InviteItem(
invitePeople = invitePeople
)
}
} }
} }
@@ -267,10 +261,12 @@ private fun RoomDetailsTopBar(
private fun MainActionsSection( private fun MainActionsSection(
state: RoomDetailsState, state: RoomDetailsState,
onShareRoom: () -> Unit, onShareRoom: () -> Unit,
onInvitePeople: () -> Unit,
onCall: () -> Unit,
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
val roomNotificationSettings = state.roomNotificationSettings val roomNotificationSettings = state.roomNotificationSettings
if (state.canShowNotificationSettings && roomNotificationSettings != null) { if (state.canShowNotificationSettings && roomNotificationSettings != null) {
@@ -292,9 +288,22 @@ private fun MainActionsSection(
) )
} }
} }
Spacer(modifier = Modifier.width(20.dp)) if (state.canCall) {
MainActionButton(
title = stringResource(CommonStrings.action_call),
imageVector = CompoundIcons.VideoCall(),
onClick = onCall,
)
}
if (state.roomType is RoomDetailsType.Room && state.canInvite) {
MainActionButton(
title = stringResource(CommonStrings.action_invite),
imageVector = CompoundIcons.UserAdd(),
onClick = onInvitePeople,
)
}
MainActionButton( MainActionButton(
title = stringResource(R.string.screen_room_details_share_room_title), title = stringResource(CommonStrings.action_share),
imageVector = CompoundIcons.ShareAndroid(), imageVector = CompoundIcons.ShareAndroid(),
onClick = onShareRoom onClick = onShareRoom
) )
@@ -410,17 +419,6 @@ private fun MembersItem(
) )
} }
@Composable
private fun InviteItem(
invitePeople: () -> Unit,
) {
ListItem(
headlineContent = { Text(stringResource(R.string.screen_room_details_invite_people_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
onClick = invitePeople,
)
}
@Composable @Composable
private fun PollsSection( private fun PollsSection(
openPollHistory: () -> Unit, openPollHistory: () -> Unit,
@@ -491,5 +489,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
openAvatarPreview = { _, _ -> }, openAvatarPreview = { _, _ -> },
openPollHistory = {}, openPollHistory = {},
openAdminSettings = {}, openAdminSettings = {},
onJoinCallClicked = {},
) )
} }

View File

@@ -62,7 +62,7 @@ class RoomDetailsViewTest {
rule.setRoomDetailView( rule.setRoomDetailView(
onShareRoom = callback, onShareRoom = callback,
) )
rule.clickOn(R.string.screen_room_details_share_room_title) rule.clickOn(CommonStrings.action_share)
} }
} }
@@ -112,9 +112,8 @@ class RoomDetailsViewTest {
} }
} }
@Config(qualifiers = "h1024dp")
@Test @Test
fun `click on invite people invokes expected callback`() { fun `click on invite invokes expected callback`() {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailView( rule.setRoomDetailView(
state = aRoomDetailsState( state = aRoomDetailsState(
@@ -123,7 +122,21 @@ class RoomDetailsViewTest {
), ),
invitePeople = callback, invitePeople = callback,
) )
rule.clickOn(R.string.screen_room_details_invite_people_title) rule.clickOn(CommonStrings.action_invite)
}
}
@Test
fun `click on call invokes expected callback`() {
ensureCalledOnce { callback ->
rule.setRoomDetailView(
state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false),
canInvite = true,
),
onJoinCallClicked = callback,
)
rule.clickOn(CommonStrings.action_call)
} }
} }
@@ -258,6 +271,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
openAvatarPreview: (name: String, url: String) -> Unit = EnsureNeverCalledWithTwoParams(), openAvatarPreview: (name: String, url: String) -> Unit = EnsureNeverCalledWithTwoParams(),
openPollHistory: () -> Unit = EnsureNeverCalled(), openPollHistory: () -> Unit = EnsureNeverCalled(),
openAdminSettings: () -> Unit = EnsureNeverCalled(), openAdminSettings: () -> Unit = EnsureNeverCalled(),
onJoinCallClicked: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
RoomDetailsView( RoomDetailsView(
@@ -272,6 +286,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
openAvatarPreview = openAvatarPreview, openAvatarPreview = openAvatarPreview,
openPollHistory = openPollHistory, openPollHistory = openPollHistory,
openAdminSettings = openAdminSettings, openAdminSettings = openAdminSettings,
onJoinCallClicked = onJoinCallClicked,
) )
} }
} }

View File

@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -53,12 +54,14 @@ fun MainActionButton(
val ripple = rememberRipple(bounded = false) val ripple = rememberRipple(bounded = false)
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Column( Column(
modifier.clickable( modifier
enabled = enabled, .clickable(
interactionSource = interactionSource, enabled = enabled,
onClick = onClick, interactionSource = interactionSource,
indication = ripple onClick = onClick,
), indication = ripple
)
.widthIn(min = 76.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
val tintColor = if (enabled) LocalContentColor.current else MaterialTheme.colorScheme.secondary val tintColor = if (enabled) LocalContentColor.current else MaterialTheme.colorScheme.secondary

View File

@@ -49,6 +49,13 @@ fun MatrixRoom.canRedactOtherAsState(updateKey: Long): State<Boolean> {
} }
} }
@Composable
fun MatrixRoom.canCall(updateKey: Long): State<Boolean> {
return produceState(initialValue = false, key1 = updateKey) {
value = canUserJoinCall(sessionId).getOrElse { false }
}
}
@Composable @Composable
fun MatrixRoom.isOwnUserAdmin(): Boolean { fun MatrixRoom.isOwnUserAdmin(): Boolean {
val roomInfo by roomInfoFlow.collectAsState(initial = null) val roomInfo by roomInfoFlow.collectAsState(initial = null)

View File

@@ -34,6 +34,7 @@
<string name="action_accept">"Accept"</string> <string name="action_accept">"Accept"</string>
<string name="action_add_to_timeline">"Add to timeline"</string> <string name="action_add_to_timeline">"Add to timeline"</string>
<string name="action_back">"Back"</string> <string name="action_back">"Back"</string>
<string name="action_call">"Call"</string>
<string name="action_cancel">"Cancel"</string> <string name="action_cancel">"Cancel"</string>
<string name="action_choose_photo">"Choose photo"</string> <string name="action_choose_photo">"Choose photo"</string>
<string name="action_clear">"Clear"</string> <string name="action_clear">"Clear"</string>