Room list: tweak UI and add overflow menu with report bug and invite friends action.

Extract invite friends action to a use case to avoid copy paste.
This commit is contained in:
Benoit Marty
2023-06-21 14:50:00 +02:00
committed by Benoit Marty
parent e7e225dd3a
commit 82e566175a
12 changed files with 180 additions and 38 deletions

View File

@@ -222,6 +222,10 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onRoomSettingsClicked(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails))
}
override fun onReportBugClicked() {
plugins<Callback>().forEach { it.onOpenBugReport() }
}
}
roomListEntryPoint
.nodeBuilder(this, buildContext)

View File

@@ -45,6 +45,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.deeplink)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.usersearch.impl)

View File

@@ -16,7 +16,7 @@
package io.element.android.features.createroom.impl.root
import android.content.Context
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -29,24 +29,18 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import timber.log.Timber
@ContributesNode(SessionScope::class)
class CreateRoomRootNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: CreateRoomRootPresenter,
private val matrixClient: MatrixClient,
private val buildMeta: BuildMeta,
private val analyticsService: AnalyticsService,
private val inviteFriendsUseCase: InviteFriendsUseCase,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
@@ -73,31 +67,18 @@ class CreateRoomRootNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val context = LocalContext.current
val activity = LocalContext.current as Activity
CreateRoomRootView(
state = state,
modifier = modifier,
onClosePressed = this::navigateUp,
onNewRoomClicked = callback::onCreateNewRoom,
onOpenDM = callback::onStartChatSuccess,
onInviteFriendsClicked = { invitePeople(context) },
onInviteFriendsClicked = { invitePeople(activity) }
)
}
private fun invitePeople(context: Context) {
val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId)
permalinkResult.onSuccess { permalink ->
val appName = buildMeta.applicationName
startSharePlainTextIntent(
context = context,
activityResultLauncher = null,
chooserTitle = context.getString(CommonStrings.action_invite_friends),
text = context.getString(CommonStrings.invite_friends_text, appName, permalink),
extraTitle = context.getString(CommonStrings.invite_friends_rich_title, appName),
noActivityFoundMessage = context.getString(io.element.android.libraries.androidutils.R.string.error_no_compatible_app_found)
)
}.onFailure {
Timber.e(it)
}
private fun invitePeople(activity: Activity) {
inviteFriendsUseCase.execute(activity)
}
}

View File

@@ -37,6 +37,7 @@ interface RoomListEntryPoint : FeatureEntryPoint {
fun onSessionVerificationClicked()
fun onInvitesClicked()
fun onRoomSettingsClicked(roomId: RoomId)
fun onReportBugClicked()
}
}

View File

@@ -48,6 +48,7 @@ dependencies {
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.eventformatter.api)
implementation(projects.libraries.deeplink)
implementation(projects.features.invitelist.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.features.leaveroom.api)

View File

@@ -16,8 +16,10 @@
package io.element.android.features.roomlist.impl
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@@ -26,6 +28,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@@ -33,7 +37,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
class RoomListNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: RoomListPresenter,
private val presenter: RoomListPresenter,
private val inviteFriendsUseCase: InviteFriendsUseCase,
) : Node(buildContext, plugins = plugins) {
private fun onRoomClicked(roomId: RoomId) {
@@ -60,9 +65,21 @@ class RoomListNode @AssistedInject constructor(
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomSettingsClicked(roomId) }
}
private fun onMenuActionClicked(activity: Activity, roomListMenuAction: RoomListMenuAction) {
when (roomListMenuAction) {
RoomListMenuAction.InviteFriends -> {
inviteFriendsUseCase.execute(activity)
}
RoomListMenuAction.ReportBug -> {
plugins<RoomListEntryPoint.Callback>().forEach { it.onReportBugClicked() }
}
}
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = LocalContext.current as Activity
RoomListView(
state = state,
onRoomClicked = this::onRoomClicked,
@@ -71,6 +88,7 @@ class RoomListNode @AssistedInject constructor(
onVerifyClicked = this::onSessionVerificationClicked,
onInvitesClicked = this::onInvitesClicked,
onRoomSettingsClicked = this::onRoomSettingsClicked,
onMenuActionClicked = { onMenuActionClicked(activity, it) },
modifier = modifier,
)
}

View File

@@ -63,6 +63,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.features.roomlist.impl.components.RoomListTopBar
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
@@ -95,6 +96,7 @@ fun RoomListView(
onCreateRoomClicked: () -> Unit,
onInvitesClicked: () -> Unit,
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
onMenuActionClicked: (RoomListMenuAction) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@@ -118,12 +120,13 @@ fun RoomListView(
RoomListContent(
state = state,
onVerifyClicked = onVerifyClicked,
onRoomClicked = onRoomClicked,
onRoomLongClicked = { onRoomLongClicked(it) },
onOpenSettings = onSettingsClicked,
onVerifyClicked = onVerifyClicked,
onCreateRoomClicked = onCreateRoomClicked,
onInvitesClicked = onInvitesClicked,
onMenuActionClicked = onMenuActionClicked,
)
// This overlaid view will only be visible when state.displaySearchResults is true
RoomListSearchResultView(
@@ -143,12 +146,13 @@ fun RoomListView(
fun RoomListContent(
state: RoomListState,
modifier: Modifier = Modifier,
onVerifyClicked: () -> Unit = {},
onRoomClicked: (RoomId) -> Unit = {},
onRoomLongClicked: (RoomListRoomSummary) -> Unit = {},
onOpenSettings: () -> Unit = {},
onCreateRoomClicked: () -> Unit = {},
onInvitesClicked: () -> Unit = {},
onVerifyClicked: () -> Unit,
onRoomClicked: (RoomId) -> Unit,
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
onOpenSettings: () -> Unit,
onCreateRoomClicked: () -> Unit,
onInvitesClicked: () -> Unit,
onMenuActionClicked: (RoomListMenuAction) -> Unit,
) {
fun onRoomClicked(room: RoomListRoomSummary) {
onRoomClicked(room.roomId)
@@ -190,6 +194,7 @@ fun RoomListContent(
areSearchResultsDisplayed = state.displaySearchResults,
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
onMenuActionClicked = onMenuActionClicked,
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
)
@@ -369,7 +374,8 @@ private fun ContentToPreview(state: RoomListState) {
onVerifyClicked = {},
onCreateRoomClicked = {},
onInvitesClicked = {},
onRoomSettingsClicked = {}
onRoomSettingsClicked = {},
onMenuActionClicked = {},
)
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.components
enum class RoomListMenuAction {
InviteFriends,
ReportBug
}

View File

@@ -19,23 +19,33 @@ package io.element.android.features.roomlist.impl.components
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -57,6 +67,7 @@ fun RoomListTopBar(
areSearchResultsDisplayed: Boolean,
onFilterChanged: (String) -> Unit,
onToggleSearch: () -> Unit,
onMenuActionClicked: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier,
@@ -79,6 +90,7 @@ fun RoomListTopBar(
matrixUser = matrixUser,
onOpenSettings = onOpenSettings,
onSearchClicked = onToggleSearch,
onMenuActionClicked = onMenuActionClicked,
scrollBehavior = scrollBehavior,
modifier = modifier,
)
@@ -90,15 +102,19 @@ private fun DefaultRoomListTopBar(
matrixUser: MatrixUser?,
scrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier,
onOpenSettings: () -> Unit = {},
onSearchClicked: () -> Unit = {},
onOpenSettings: () -> Unit,
onSearchClicked: () -> Unit,
onMenuActionClicked: (RoomListMenuAction) -> Unit,
) {
var showMenu by remember { mutableStateOf(false) }
MediumTopAppBar(
modifier = modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
title = {
val fontSize = if (scrollBehavior.state.collapsedFraction > 0.5) 20.sp else 22.sp
Text(
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.headlineMedium.copy(fontSize = fontSize),
text = stringResource(id = R.string.screen_roomlist_main_space_title)
)
},
@@ -108,7 +124,11 @@ private fun DefaultRoomListTopBar(
modifier = Modifier.testTag(TestTags.homeScreenSettings),
onClick = onOpenSettings
) {
val avatarData by remember { derivedStateOf { matrixUser.getAvatarData() } }
val avatarData by remember {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.Custom(28.dp))
}
}
Avatar(avatarData, contentDescription = stringResource(CommonStrings.common_settings))
}
}
@@ -119,6 +139,32 @@ private fun DefaultRoomListTopBar(
) {
Icon(Icons.Default.Search, contentDescription = stringResource(CommonStrings.action_search))
}
IconButton(
onClick = { showMenu = !showMenu }
) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClicked(RoomListMenuAction.InviteFriends)
},
text = { Text(stringResource(id = CommonStrings.action_invite)) },
leadingIcon = { Icon(Icons.Default.Share, contentDescription = null) }
)
DropdownMenuItem(
onClick = {
showMenu = false
onMenuActionClicked(RoomListMenuAction.ReportBug)
},
text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) },
leadingIcon = { Icon(Icons.Default.BugReport, contentDescription = null) }
)
}
},
scrollBehavior = scrollBehavior,
windowInsets = WindowInsets(0.dp),
@@ -139,5 +185,8 @@ private fun DefaultRoomListTopBarPreview() {
DefaultRoomListTopBar(
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
onOpenSettings = {},
onSearchClicked = {},
onMenuActionClicked = {},
)
}

View File

@@ -30,8 +30,12 @@ dependencies {
implementation(projects.libraries.di)
implementation(libs.dagger)
implementation(libs.androidx.corektx)
implementation(projects.libraries.core)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.uiStrings)
implementation(projects.services.toolbox.api)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)

View File

@@ -0,0 +1,54 @@
/*
* 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.deeplink.usecase
import android.app.Activity
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.services.toolbox.api.strings.StringProvider
import timber.log.Timber
import javax.inject.Inject
import io.element.android.libraries.androidutils.R as AndroidUtilsR
import io.element.android.libraries.ui.strings.CommonStrings
class InviteFriendsUseCase @Inject constructor(
private val stringProvider: StringProvider,
private val matrixClient: MatrixClient,
private val buildMeta: BuildMeta,
) {
fun execute(activity: Activity) {
val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId)
permalinkResult.fold(
onSuccess = { permalink ->
val appName = buildMeta.applicationName
startSharePlainTextIntent(
context = activity,
activityResultLauncher = null,
chooserTitle = stringProvider.getString(CommonStrings.action_invite_friends),
text = stringProvider.getString(CommonStrings.invite_friends_text, appName, permalink),
extraTitle = stringProvider.getString(CommonStrings.invite_friends_rich_title, appName),
noActivityFoundMessage = stringProvider.getString(AndroidUtilsR.string.error_no_compatible_app_found)
)
},
onFailure = {
Timber.e(it)
}
)
}
}

View File

@@ -103,6 +103,7 @@ class RoomListScreen(
onCreateRoomClicked = {},
onInvitesClicked = {},
onRoomSettingsClicked = {},
onMenuActionClicked = {},
modifier = modifier,
)