feat: Add UserInfoSheet and enhance ChannelSideDrawer with user context functionality

This commit introduces a new UserInfoSheet for displaying user details and updates the ChannelSideDrawer to support user context interactions.

- Implemented UserInfoSheet to fetch and display user profiles, including error handling for user retrieval.
- Enhanced ChannelSideDrawer to manage user context sheets, allowing users to view detailed information about other users.
- Updated existing components to replace channel context interactions with user context interactions where applicable.
- Refactored related functions to maintain clean architecture principles and improve code organization.
This commit is contained in:
AbronStudio 2025-08-19 12:01:38 +03:30
parent bd4fd09559
commit 48513b761b
3 changed files with 80 additions and 32 deletions

View File

@ -207,7 +207,7 @@ class ShareTargetScreenViewModel @Inject constructor(
}
)
attachmentIds.add(id)
} catch (e: Exception) {
} catch (_: Exception) {
return@launch
}
}
@ -351,7 +351,8 @@ fun ShareTargetScreen(
isCurrent = selectedChannel == channel.id,
hasUnread = false,
onDestinationChanged = { selectedChannel = channel.id },
onOpenChannelContextSheet = {}
onOpenChannelContextSheet = {},
onOpenUserInfoSheet = {}
)
else -> ChannelItem(

View File

@ -96,6 +96,7 @@ import chat.peptide.composables.screens.chat.ChannelIcon
import chat.peptide.composables.screens.chat.discover.DiscoverServersList
import chat.peptide.screens.chat.ChatRouterDestination
import chat.peptide.sheets.ChannelContextSheet
import chat.peptide.sheets.UserInfoSheet
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
@ -169,6 +170,7 @@ fun ChannelSideDrawer(
}.sortedBy { it.id }))
var channelContextSheetTarget by remember { mutableStateOf<String?>(null) }
var userContextSheetTarget by remember { mutableStateOf<String?>(null) }
if (channelContextSheetTarget != null) {
val channelContextSheetState = rememberModalBottomSheetState()
@ -189,6 +191,25 @@ fun ChannelSideDrawer(
}
}
if (userContextSheetTarget != null) {
val userContextSheetState = rememberModalBottomSheetState()
ModalBottomSheet(
sheetState = userContextSheetState,
onDismissRequest = {
userContextSheetTarget = null
}
) {
UserInfoSheet(
userId = userContextSheetTarget!!,
serverId = null,
dismissSheet = {
userContextSheetState.hide()
userContextSheetTarget = null
}
)
}
}
Box {
@ -561,7 +582,7 @@ fun ChannelSideDrawer(
currentDestination,
onDestinationChanged,
channelListState,
onOpenChannelContextSheet = { channelContextSheetTarget = it }
onOpenUserInfoSheet = { userId -> userContextSheetTarget = userId }
)
} else {
ServerChannelListRenderer(
@ -647,7 +668,7 @@ private fun DirectMessagesChannelListRendererPreview() {
currentDestination = ChatRouterDestination.Home,
onDestinationChanged = {},
channelListState = rememberLazyListState(),
onOpenChannelContextSheet = {}
onOpenUserInfoSheet = {}
)
}
}
@ -670,7 +691,7 @@ private fun DirectMessagesChannelListRendererEmptyPreview() {
currentDestination = ChatRouterDestination.Home,
onDestinationChanged = {},
channelListState = rememberLazyListState(),
onOpenChannelContextSheet = {}
onOpenUserInfoSheet = {}
)
}
}
@ -680,7 +701,7 @@ private fun ColumnScope.DirectMessagesChannelListRenderer(
currentDestination: ChatRouterDestination,
onDestinationChanged: (ChatRouterDestination) -> Unit,
channelListState: LazyListState,
onOpenChannelContextSheet: (String) -> Unit,
onOpenUserInfoSheet: (String) -> Unit,
) {
val dmAbleChannels =
PeptideAPI.channelCache.values
@ -805,7 +826,8 @@ private fun ColumnScope.DirectMessagesChannelListRenderer(
onDestinationChanged = { dest ->
onDestinationChanged(dest)
},
onOpenChannelContextSheet = onOpenChannelContextSheet
onOpenChannelContextSheet = onOpenUserInfoSheet,
onOpenUserInfoSheet = onOpenUserInfoSheet
)
}
}
@ -1042,7 +1064,8 @@ fun DMOrGroupItem(
hasUnread: Boolean,
isMuted: Boolean = false,
onDestinationChanged: (ChatRouterDestination) -> Unit,
onOpenChannelContextSheet: (String) -> Unit
onOpenChannelContextSheet: (String) -> Unit,
onOpenUserInfoSheet: (String) -> Unit
) {
val currentIndicatorOpacity = animateFloatAsState(
targetValue = if (isCurrent) 1f else 0f,
@ -1060,9 +1083,12 @@ fun DMOrGroupItem(
.combinedClickable(
onLongClickLabel = stringResource(R.string.channel_context_sheet_open),
onLongClick = {
channel.id?.let { chId ->
partner?.id?.let { userId ->
onOpenUserInfoSheet(userId)
} ?: channel.id?.let { chId ->
onOpenChannelContextSheet(chId)
}
},
onClick = {
channel.id?.let { chId ->
@ -1122,7 +1148,18 @@ fun DMOrGroupItem(
avatar = partner?.avatar ?: channel.icon,
size = 28.dp,
presenceSize = 12.dp
)
).let { avatar ->
// Placed inside a Box, otherwise the combinedClickable modifier doesn't work.
Box(
Modifier.combinedClickable(
onLongClick = {
partner?.id?.let { uid -> onOpenUserInfoSheet(uid) }
},
onClick = {}
),
content = { avatar }
)
}
}
Column(Modifier.weight(1f)) {

View File

@ -44,6 +44,7 @@ import chat.peptide.api.internals.BrushCompat
import chat.peptide.api.internals.ULID
import chat.peptide.api.internals.solidColor
import chat.peptide.api.routes.user.fetchUserProfile
import chat.peptide.api.routes.user.getOrFetchUser
import chat.peptide.api.schemas.Profile
import chat.peptide.api.settings.Experiments
import chat.peptide.api.settings.FeatureFlags
@ -65,7 +66,17 @@ fun UserInfoSheet(
serverId: String? = null,
dismissSheet: suspend () -> Unit
) {
val user = PeptideAPI.userCache[userId]
var resolvedUser by remember { mutableStateOf(PeptideAPI.userCache[userId]) }
LaunchedEffect(userId) {
try {
if (resolvedUser == null) {
resolvedUser = getOrFetchUser(userId)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
val member = serverId?.let { PeptideAPI.members.getMember(it, userId) }
@ -74,9 +85,9 @@ fun UserInfoSheet(
var profile by remember { mutableStateOf<Profile?>(null) }
var profileNotFound by remember { mutableStateOf(false) }
LaunchedEffect(user) {
LaunchedEffect(resolvedUser) {
try {
user?.id?.let { fetchUserProfile(it) }?.let { profile = it }
resolvedUser?.id?.let { fetchUserProfile(it) }?.let { profile = it }
} catch (e: Exception) {
if (e.message == "NotFound") {
profileNotFound = true
@ -85,8 +96,7 @@ fun UserInfoSheet(
}
}
if (user == null) {
// TODO fetch user in this scenario
if (resolvedUser == null) {
NonIdealState(
icon = {
Icon(
@ -117,7 +127,7 @@ fun UserInfoSheet(
sheetState = sheetState,
onDismissRequest = { showUserCard = false }
) {
UserCardSheet(user)
UserCardSheet(resolvedUser!!)
}
}
@ -129,7 +139,7 @@ fun UserInfoSheet(
onDismissRequest = { showServerIdentityOptions = false }
) {
ServerIdentityOptionsSheet(
userId = user.id!!
userId = resolvedUser!!.id!!
)
}
}
@ -142,7 +152,7 @@ fun UserInfoSheet(
) {
item(key = "overview", span = StaggeredGridItemSpan.FullLine) {
Box {
RawUserOverview(user, profile, internalPadding = false)
RawUserOverview(resolvedUser!!, profile, internalPadding = false)
Row(
modifier = Modifier
.align(Alignment.TopEnd)
@ -214,9 +224,9 @@ fun UserInfoSheet(
}
}
}
val accountAt = user.id?.let {
val accountAt = resolvedUser!!.id?.let {
DateUtils.getRelativeTimeSpanString(
ULID.asTimestamp(user.id),
ULID.asTimestamp(resolvedUser!!.id!!),
System.currentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS
).toString()
@ -292,22 +302,22 @@ fun UserInfoSheet(
}
}
if ((user.badges ?: 0) > 0) {
if ((resolvedUser?.badges ?: 0) > 0) {
item(key = "info") {
SheetTile(
header = {
Text(stringResource(R.string.user_info_sheet_category_badges))
},
contentPreview = {
user.badges?.let { UserBadgeRow(badges = it) }
resolvedUser?.badges?.let { UserBadgeRow(badges = it) }
}
) {
user.badges?.let { UserBadgeList(badges = it) }
resolvedUser?.badges?.let { UserBadgeList(badges = it) }
}
}
}
if (user.status?.text != null) {
if (resolvedUser?.status?.text != null) {
item(key = "status") {
SheetTile(
header = {
@ -315,7 +325,7 @@ fun UserInfoSheet(
},
contentPreview = {
Text(
text = user.status.text,
text = resolvedUser!!.status!!.text ?: "",
fontSize = 14.sp,
maxLines = 5,
overflow = TextOverflow.Ellipsis
@ -323,15 +333,15 @@ fun UserInfoSheet(
}
) {
Text(
text = user.status.text,
text = resolvedUser!!.status!!.text ?: "",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
if (user.bot != null) {
val resolvedOwner = user.bot.owner?.let { PeptideAPI.userCache[it] }
if (resolvedUser?.bot != null) {
val resolvedOwner = resolvedUser!!.bot!!.owner?.let { PeptideAPI.userCache[it] }
item(key = "bot-owner") {
SheetTile(
@ -345,7 +355,7 @@ fun UserInfoSheet(
resolvedOwner?.let {
UserAvatar(
username = it.displayName ?: it.username
?: stringResource(R.string.unknown),
?: stringResource(R.string.unknown),
avatar = it.avatar,
userId = it.id!!,
size = 32.dp
@ -353,7 +363,7 @@ fun UserInfoSheet(
Spacer(modifier = Modifier.width(8.dp))
Text(
text = it.displayName ?: it.username
?: stringResource(R.string.unknown),
?: stringResource(R.string.unknown),
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@ -380,7 +390,7 @@ fun UserInfoSheet(
NonIdealState(
icon = {
Icon(
painter = painterResource(R.drawable.icn_error_24dp),
painter = painterResource(id = R.drawable.icn_error_24dp),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
@ -414,7 +424,7 @@ fun UserInfoSheet(
}
item(key = "actions", span = StaggeredGridItemSpan.FullLine) {
UserButtons(user, dismissSheet)
UserButtons(resolvedUser!!, dismissSheet)
}
}