Skip to content

feat: add team management access from main navigation (WPB-17721) #4092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 2, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ sealed class HomeDestination(
direction = WhatsNewScreenDestination
)

data object TeamManagement : HomeDestination(
title = UIText.StringResource(R.string.team_management_screen_title),
icon = R.drawable.ic_team_management,
direction = TeamManagementScreenDestination
)

data object Cells : HomeDestination(
title = UIText.StringResource(R.string.cells_screen_title),
icon = R.drawable.ic_files,
Expand All @@ -100,6 +106,6 @@ sealed class HomeDestination(
values().find { it.direction.route.getBaseRoute() == fullRoute.getBaseRoute() }

fun values(): Array<HomeDestination> =
arrayOf(Conversations, Settings, Vault, Archive, Support, WhatsNew, Cells)
arrayOf(Conversations, Settings, Vault, Archive, Support, TeamManagement, WhatsNew, Cells)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ import com.wire.android.util.getUrisOfFilesInDirectory
import com.wire.android.util.multipleFileSharingIntent
import com.wire.android.util.sha256

/**
* The route is not that relevant as won't be used for navigation, it will be overridden by a direct custom tab launch.
*/
interface ExternalDirectionLess : Direction {
override val route: String
get() = this::class.qualifiedName.orEmpty()
}

interface ExternalUriDirection : Direction {
val uri: Uri
override val route: String
Expand All @@ -57,6 +65,8 @@ object SupportScreenDestination : ExternalUriStringResDirection {
get() = R.string.url_support
}

data object TeamManagementScreenDestination : ExternalDirectionLess

object PrivacyPolicyScreenDestination : ExternalUriStringResDirection {
override val uriStringRes: Int
get() = R.string.url_privacy_policy
Expand Down
109 changes: 78 additions & 31 deletions app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand All @@ -39,13 +43,17 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.wire.android.R
import com.wire.android.navigation.ExternalDirectionLess
import com.wire.android.navigation.ExternalUriDirection
import com.wire.android.navigation.ExternalUriStringResDirection
import com.wire.android.navigation.HomeDestination
import com.wire.android.ui.common.Logo
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.selectableBackground
import com.wire.android.ui.common.spacers.HorizontalSpace
Expand Down Expand Up @@ -81,40 +89,56 @@ fun HomeDrawer(
.height(MaterialTheme.wireDimensions.homeDrawerLogoHeight)
)

fun navigateAndCloseDrawer(item: HomeDestination) {
navigateToHomeItem(item)
onCloseDrawer()
val (topItems, bottomItems) = homeDrawerState.items
topItems.forEach { item ->
MapToDrawerItem(navigateToHomeItem, onCloseDrawer, currentRoute, item)
}

DrawerItem(
destination = HomeDestination.Conversations,
selected = currentRoute == HomeDestination.Conversations.direction.route,
onItemClick = remember { { navigateAndCloseDrawer(HomeDestination.Conversations) } }
)
Spacer(modifier = Modifier.weight(1f))

if (homeDrawerState.showFilesOption) {
DrawerItem(
destination = HomeDestination.Cells,
selected = currentRoute == HomeDestination.Cells.direction.route,
onItemClick = remember { { navigateAndCloseDrawer(HomeDestination.Cells) } }
)
bottomItems.forEach { item ->
MapToDrawerItem(navigateToHomeItem, onCloseDrawer, currentRoute, item)
}
}
}

DrawerItem(
destination = HomeDestination.Archive,
unreadCount = homeDrawerState.unreadArchiveConversationsCount,
selected = currentRoute == HomeDestination.Archive.direction.route,
onItemClick = remember { { navigateAndCloseDrawer(HomeDestination.Archive) } }
)
@Composable
fun MapToDrawerItem(
navigateToHomeItem: (HomeDestination) -> Unit,
onCloseDrawer: () -> Unit,
currentRoute: String?,
drawerUiItem: DrawerUiItem
) {
val context = LocalContext.current
fun navigateAndCloseDrawer(item: HomeDestination) {
navigateToHomeItem(item)
onCloseDrawer()
}

Spacer(modifier = Modifier.weight(1f))
with(drawerUiItem) {
when (this) {
is DrawerUiItem.DynamicExternalNavigationItem -> DrawerItem(
destination = destination,
selected = currentRoute == destination.direction.route,
onItemClick = remember {
{
com.wire.android.util.CustomTabsHelper.launchUrl(context, url)
onCloseDrawer()
}
}
)

val bottomItems = listOf(HomeDestination.WhatsNew, HomeDestination.Settings, HomeDestination.Support)
bottomItems.forEach { item ->
DrawerItem(
destination = item,
selected = currentRoute == item.direction.route,
onItemClick = remember { { navigateAndCloseDrawer(item) } }
is DrawerUiItem.RegularItem -> DrawerItem(
destination = destination,
selected = currentRoute == destination.direction.route,
onItemClick = remember { { navigateAndCloseDrawer(destination) } }
)

is DrawerUiItem.UnreadCounterItem -> DrawerItem(
destination = destination,
unreadCount = this.unreadCount.toInt(),
selected = currentRoute == destination.direction.route,
onItemClick = remember { { navigateAndCloseDrawer(destination) } }
)
}
}
Expand All @@ -133,10 +157,10 @@ fun DrawerItem(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.padding(bottom = 8.dp)
.clip(RoundedCornerShape(12.dp))
.padding(bottom = dimensions().spacing8x)
.clip(RoundedCornerShape(dimensions().spacing12x))
.fillMaxWidth()
.height(40.dp)
.height(dimensions().spacing40x)
.background(backgroundColor)
.selectableBackground(selected, stringResource(R.string.content_description_open_label), onItemClick),
) {
Expand All @@ -157,6 +181,17 @@ fun DrawerItem(
.weight(1F)
)
UnreadMessageEventBadge(unreadMessageCount = unreadCount)
with(destination) {
if (direction is ExternalUriDirection || direction is ExternalUriStringResDirection || direction is ExternalDirectionLess) {
HorizontalSpace.x8()
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = null,
tint = colorsScheme().secondaryText,
modifier = Modifier.size(dimensions().spacing16x)
)
}
}
HorizontalSpace.x12()
}
}
Expand Down Expand Up @@ -186,3 +221,15 @@ fun PreviewUnSelectedArchivedItemWithUnreadCount() {
)
}
}

@PreviewMultipleThemes
@Composable
fun PreviewItemWithExternalDestination() {
Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surface)) {
DrawerItem(
destination = HomeDestination.Support,
selected = false,
onItemClick = {},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
package com.wire.android.ui.home.drawer

data class HomeDrawerState(
val unreadArchiveConversationsCount: Int,
val showFilesOption: Boolean,
/**
* The items to be displayed in the drawer [Pair] of "top" and "bottom" items.
*/
val items: Pair<List<DrawerUiItem>, List<DrawerUiItem>> = emptyList<DrawerUiItem>() to emptyList<DrawerUiItem>()
)
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,100 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.navigation.HomeDestination
import com.wire.android.util.EMPTY
import com.wire.kalium.logic.data.user.type.UserType
import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversationsCountUseCase
import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase
import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject

@Suppress("LongParameterList")
@HiltViewModel
class HomeDrawerViewModel @Inject constructor(
val savedStateHandle: SavedStateHandle,
private val observeArchivedUnreadConversationsCountUseCase: ObserveArchivedUnreadConversationsCountUseCase,
private val observeArchivedUnreadConversationsCount: ObserveArchivedUnreadConversationsCountUseCase,
private val observeSelfUser: ObserveSelfUserUseCase,
private val getTeamUrl: GetTeamUrlUseCase,
private val globalDataStore: GlobalDataStore,
) : ViewModel() {

var drawerState by mutableStateOf(
HomeDrawerState(
unreadArchiveConversationsCount = 0,
showFilesOption = false,
)
)
var drawerState by mutableStateOf(HomeDrawerState())
private set

init {
observeUnreadArchiveConversationsCount()
observeWireCellsFeatureState()
buildDrawerItems()
}

private fun observeWireCellsFeatureState() = viewModelScope.launch {
globalDataStore.wireCellsEnabled().collect {
drawerState = drawerState.copy(showFilesOption = it)
private suspend fun observeTeamManagementUrlForUser(): Flow<String> {
return observeSelfUser().map {
when (it.userType) {
UserType.ADMIN,
UserType.OWNER -> {
getTeamUrl()
}

UserType.INTERNAL,
UserType.EXTERNAL,
UserType.FEDERATED,
UserType.GUEST,
UserType.SERVICE,
UserType.NONE -> {
String.EMPTY
}
}
}
}

private fun observeUnreadArchiveConversationsCount() {
private fun buildDrawerItems() {
viewModelScope.launch {
observeArchivedUnreadConversationsCountUseCase()
.collect { drawerState = drawerState.copy(unreadArchiveConversationsCount = it.toInt()) }
combine(
globalDataStore.wireCellsEnabled(),
observeArchivedUnreadConversationsCount(),
observeTeamManagementUrlForUser()
) { wireCellsEnabled, unreadArchiveConversationsCount, teamManagementUrl ->
buildList {
add(DrawerUiItem.RegularItem(destination = HomeDestination.Conversations))
if (wireCellsEnabled) {
add(DrawerUiItem.RegularItem(destination = HomeDestination.Cells))

Check warning on line 88 in app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModel.kt#L88

Added line #L88 was not covered by tests
}
add(
DrawerUiItem.UnreadCounterItem(
destination = HomeDestination.Archive,
unreadCount = unreadArchiveConversationsCount
)
)
} to buildList {
add(DrawerUiItem.RegularItem(destination = HomeDestination.WhatsNew))
add(DrawerUiItem.RegularItem(destination = HomeDestination.Settings))
if (teamManagementUrl.isNotBlank()) {
add(
DrawerUiItem.DynamicExternalNavigationItem(
destination = HomeDestination.TeamManagement,
url = teamManagementUrl
)
)
}
add(DrawerUiItem.RegularItem(destination = HomeDestination.Support))
}
}.collect {
drawerState = drawerState.copy(items = it)
}
}
}
}

/**
* The type of the main navigation item.
* Regular, with counter or with external navigation.
*/
sealed class DrawerUiItem(open val destination: HomeDestination) {
data class RegularItem(override val destination: HomeDestination) : DrawerUiItem(destination)
data class UnreadCounterItem(override val destination: HomeDestination, val unreadCount: Long) : DrawerUiItem(destination)
data class DynamicExternalNavigationItem(override val destination: HomeDestination, val url: String) : DrawerUiItem(destination)
}
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ fun PersonalSelfUserProfileScreenPreview() {
fullName = "Some User",
userName = "some-user",
teamName = null,
teamUrl = "some-url",
otherAccounts = listOf(
OtherAccount(
id = UserId("id1", "domain"),
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_team_management.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M2.909,5.002V10.998L8,14.013L13.091,10.998V5.002L8,1.987L2.909,5.002ZM7.17,0.203C7.628,-0.069 8.376,-0.066 8.83,0.203L14.17,3.364C14.628,3.635 15,4.301 15,4.839V11.161C15,11.704 14.624,12.367 14.17,12.636L8.83,15.797C8.372,16.069 7.624,16.066 7.17,15.797L1.83,12.636C1.372,12.365 1,11.699 1,11.161V4.839C1,4.296 1.376,3.633 1.83,3.364L7.17,0.203Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@
<string name="backup_and_restore_screen_title">Back up &amp; Restore Conversations</string>
<string name="search_bar_conversations_hint">Search conversations</string>
<string name="search_no_results">No matches found</string>
<string name="team_management_screen_title">Team Management</string>
<!--Whats New -->
<string name="whats_new_screen_title">What\'s new</string>
<string name="whats_new_welcome_to_new_android_app_label">👋 Welcome to Wire\'s New Android App!</string>
Expand Down
Loading
Loading