From 55e8d184f4e2c5af6e24a154e97aac54c33b7873 Mon Sep 17 00:00:00 2001 From: Infi Date: Mon, 30 Oct 2023 02:18:26 +0100 Subject: [PATCH] feat: friends screen Signed-off-by: Infi --- .../revolt/api/internals/FriendRequests.kt | 36 +++ .../revolt/api/realtime/RealtimeSocket.kt | 22 ++ .../revolt/api/routes/user/Relationships.kt | 17 +- .../revolt/components/generic/PageHeader.kt | 2 + .../chat/drawer/channel/ChannelList.kt | 38 ++- .../chat/drawer/server/DrawerChannel.kt | 83 ++++-- .../revolt/screens/chat/ChatRouterScreen.kt | 67 +++-- .../screens/chat/views/FriendsScreen.kt | 276 ++++++++++++++++++ .../revolt/screens/chat/views/HomeScreen.kt | 18 +- app/src/main/res/drawable/ic_home_24dp.xml | 9 + app/src/main/res/values/strings.xml | 6 + 11 files changed, 506 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/chat/revolt/api/internals/FriendRequests.kt create mode 100644 app/src/main/java/chat/revolt/screens/chat/views/FriendsScreen.kt create mode 100644 app/src/main/res/drawable/ic_home_24dp.xml diff --git a/app/src/main/java/chat/revolt/api/internals/FriendRequests.kt b/app/src/main/java/chat/revolt/api/internals/FriendRequests.kt new file mode 100644 index 00000000..81264a13 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/internals/FriendRequests.kt @@ -0,0 +1,36 @@ +package chat.revolt.api.internals + +import chat.revolt.api.RevoltAPI +import chat.revolt.api.schemas.User + +object FriendRequests { + fun getIncoming(): List { + return RevoltAPI.userCache.values.filter { user -> + user.relationship == "Incoming" + } + } + + fun getIncomingCount(): Int { + return getIncoming().size + } + + fun getOutgoing(): List { + return RevoltAPI.userCache.values.filter { user -> + user.relationship == "Outgoing" + } + } + + fun getOutgoingCount(): Int { + return getOutgoing().size + } + + fun getBlocked(): List { + return RevoltAPI.userCache.values.filter { user -> + user.relationship == "Blocked" + } + } + + fun getBlockedCount(): Int { + return getBlocked().size + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt index b0c3d666..264a3d16 100644 --- a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt +++ b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt @@ -23,6 +23,7 @@ import chat.revolt.api.realtime.frames.receivable.ServerMemberJoinFrame import chat.revolt.api.realtime.frames.receivable.ServerMemberLeaveFrame import chat.revolt.api.realtime.frames.receivable.ServerMemberUpdateFrame import chat.revolt.api.realtime.frames.receivable.ServerUpdateFrame +import chat.revolt.api.realtime.frames.receivable.UserRelationshipFrame import chat.revolt.api.realtime.frames.receivable.UserUpdateFrame import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame import chat.revolt.api.realtime.frames.sendable.PingFrame @@ -262,6 +263,27 @@ object RealtimeSocket { existing.mergeWithPartial(userUpdateFrame.data) } + "UserRelationship" -> { + val userRelationshipFrame = + RevoltJson.decodeFromString(UserRelationshipFrame.serializer(), rawFrame) + + val existing = RevoltAPI.userCache[userRelationshipFrame.user.id] + + if (existing == null && userRelationshipFrame.user.id != null) { + RevoltAPI.userCache[userRelationshipFrame.user.id] = + userRelationshipFrame.user.copy( + relationship = userRelationshipFrame.status + ) + } else if (existing != null && userRelationshipFrame.user.id != null) { + val merged = existing.mergeWithPartial(userRelationshipFrame.user).copy( + relationship = userRelationshipFrame.status + ) + RevoltAPI.userCache[userRelationshipFrame.user.id] = merged + } else { + Log.w("RealtimeSocket", "Invalid UserRelationship frame: $rawFrame") + } + } + "ChannelUpdate" -> { val channelUpdateFrame = RevoltJson.decodeFromString(ChannelUpdateFrame.serializer(), rawFrame) diff --git a/app/src/main/java/chat/revolt/api/routes/user/Relationships.kt b/app/src/main/java/chat/revolt/api/routes/user/Relationships.kt index 05f89313..bb03a0ac 100644 --- a/app/src/main/java/chat/revolt/api/routes/user/Relationships.kt +++ b/app/src/main/java/chat/revolt/api/routes/user/Relationships.kt @@ -7,8 +7,8 @@ import chat.revolt.api.RevoltJson import io.ktor.client.request.delete import io.ktor.client.request.put import io.ktor.client.statement.bodyAsText -import kotlin.collections.set import kotlinx.serialization.SerializationException +import kotlin.collections.set suspend fun blockUser(userId: String) { val response = RevoltHttp.put("/users/$userId/block") @@ -39,3 +39,18 @@ suspend fun unblockUser(userId: String) { val user = RevoltAPI.userCache[userId] ?: return RevoltAPI.userCache[userId] = user.copy(relationship = "None") } + +suspend fun unfriendUser(userId: String) { + val response = RevoltHttp.delete("/users/$userId/friend") + .bodyAsText() + + try { + val error = RevoltJson.decodeFromString(RevoltError.serializer(), response) + throw Error(error.type) + } catch (e: SerializationException) { + // Not an error + } + + val user = RevoltAPI.userCache[userId] ?: return + RevoltAPI.userCache[userId] = user.copy(relationship = "None") +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/generic/PageHeader.kt b/app/src/main/java/chat/revolt/components/generic/PageHeader.kt index 7df330c9..f8c0eece 100644 --- a/app/src/main/java/chat/revolt/components/generic/PageHeader.kt +++ b/app/src/main/java/chat/revolt/components/generic/PageHeader.kt @@ -27,6 +27,7 @@ fun PageHeader( modifier: Modifier = Modifier, showBackButton: Boolean = false, onBackButtonClicked: () -> Unit = {}, + startButtons: @Composable () -> Unit = {}, additionalButtons: @Composable () -> Unit = {}, maxLines: Int = Int.MAX_VALUE ) { @@ -42,6 +43,7 @@ fun PageHeader( ) } } + startButtons() Text( text = text, maxLines = maxLines, diff --git a/app/src/main/java/chat/revolt/components/screens/chat/drawer/channel/ChannelList.kt b/app/src/main/java/chat/revolt/components/screens/chat/drawer/channel/ChannelList.kt index 3f942f24..7fc24943 100644 --- a/app/src/main/java/chat/revolt/components/screens/chat/drawer/channel/ChannelList.kt +++ b/app/src/main/java/chat/revolt/components/screens/chat/drawer/channel/ChannelList.kt @@ -68,6 +68,7 @@ import chat.revolt.api.schemas.User import chat.revolt.api.schemas.has import chat.revolt.components.generic.presenceFromStatus import chat.revolt.components.screens.chat.drawer.server.DrawerChannel +import chat.revolt.components.screens.chat.drawer.server.DrawerChannelIconType import chat.revolt.sheets.ChannelContextSheet import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions @@ -90,7 +91,7 @@ fun RowScope.ChannelList( val enableSmallBanner by remember { derivedStateOf { lazyListState.firstVisibleItemScrollOffset > 40 || - lazyListState.firstVisibleItemIndex > 0 + lazyListState.firstVisibleItemIndex > 0 } } @@ -186,7 +187,7 @@ fun RowScope.ChannelList( ) { DrawerChannel( name = stringResource(R.string.home), - channelType = ChannelType.TextChannel, + iconType = DrawerChannelIconType.Painter(painterResource(R.drawable.ic_home_24dp)), selected = currentDestination == "home", hasUnread = false, onClick = { @@ -196,6 +197,21 @@ fun RowScope.ChannelList( ) } + item( + key = "friends" + ) { + DrawerChannel( + name = stringResource(R.string.friends), + iconType = DrawerChannelIconType.Painter(painterResource(R.drawable.ic_human_greeting_variant_24dp)), + selected = currentDestination == "friends", + hasUnread = false, + onClick = { + onSpecialClick("friends") + }, + large = true + ) + } + item( key = "notes" ) { @@ -204,7 +220,7 @@ fun RowScope.ChannelList( DrawerChannel( name = stringResource(R.string.channel_notes), - channelType = ChannelType.SavedMessages, + iconType = DrawerChannelIconType.Channel(ChannelType.SavedMessages), selected = currentDestination == "channel/{channelId}" && currentChannel == notesChannelId, hasUnread = false, onClick = { @@ -248,8 +264,10 @@ fun RowScope.ChannelList( DrawerChannel( name = partner?.let { p -> User.resolveDefaultName(p) } ?: channel.name - ?: stringResource(R.string.unknown), - channelType = channel.channelType ?: ChannelType.TextChannel, + ?: stringResource(R.string.unknown), + iconType = DrawerChannelIconType.Channel( + channel.channelType ?: ChannelType.TextChannel + ), selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id, hasUnread = channel.lastMessageID?.let { lastMessageID -> RevoltAPI.unreads.hasUnread( @@ -408,9 +426,9 @@ fun RowScope.ChannelList( Text( text = ( - server?.name - ?: stringResource(R.string.unknown) - ), + server?.name + ?: stringResource(R.string.unknown) + ), style = MaterialTheme.typography.labelLarge, color = if (server?.banner != null) { bannerTextColour @@ -513,7 +531,9 @@ fun RowScope.ChannelList( name = partner?.let { p -> User.resolveDefaultName(p) } ?: channel.name ?: stringResource(R.string.unknown), - channelType = channel.channelType ?: ChannelType.TextChannel, + iconType = DrawerChannelIconType.Channel( + channel.channelType ?: ChannelType.TextChannel + ), selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id, hasUnread = channel.lastMessageID?.let { lastMessageID -> RevoltAPI.unreads.hasUnread( diff --git a/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerChannel.kt b/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerChannel.kt index e63e73ea..1d796c2f 100644 --- a/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerChannel.kt +++ b/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerChannel.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -33,10 +34,16 @@ import chat.revolt.components.generic.Presence import chat.revolt.components.generic.UserAvatar import chat.revolt.components.screens.chat.ChannelIcon +sealed class DrawerChannelIconType { + data class Channel(val type: ChannelType) : DrawerChannelIconType() + data class Painter(val painter: androidx.compose.ui.graphics.painter.Painter) : + DrawerChannelIconType() +} + @OptIn(ExperimentalFoundationApi::class) @Composable fun DrawerChannel( - channelType: ChannelType, + iconType: DrawerChannelIconType, name: String, selected: Boolean, hasUnread: Boolean, @@ -84,39 +91,55 @@ fun DrawerChannel( .padding(vertical = 8.dp, horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - when (channelType) { - ChannelType.DirectMessage -> UserAvatar( - username = dmPartnerName ?: "", - avatar = dmPartnerIcon, - userId = dmPartnerId ?: "", - presence = dmPartnerStatus, - size = 32.dp, - presenceSize = 16.dp, - modifier = Modifier.padding(end = 8.dp) - ) + when (iconType) { + is DrawerChannelIconType.Channel -> { + when (val channelType = iconType.type) { + ChannelType.DirectMessage -> UserAvatar( + username = dmPartnerName ?: "", + avatar = dmPartnerIcon, + userId = dmPartnerId ?: "", + presence = dmPartnerStatus, + size = 32.dp, + presenceSize = 16.dp, + modifier = Modifier.padding(end = 8.dp) + ) - ChannelType.Group -> GroupIcon( - name = name, - icon = dmPartnerIcon, - size = 32.dp, - modifier = Modifier.padding(end = 8.dp) - ) + ChannelType.Group -> GroupIcon( + name = name, + icon = dmPartnerIcon, + size = 32.dp, + modifier = Modifier.padding(end = 8.dp) + ) - else -> ChannelIcon( - channelType = channelType, - modifier = Modifier.then( - if (large) { - Modifier.padding( - end = 12.dp, - start = 4.dp, - top = 4.dp, - bottom = 4.dp + else -> ChannelIcon( + channelType = channelType, + modifier = Modifier.then( + if (large) { + Modifier.padding( + end = 12.dp, + start = 4.dp, + top = 4.dp, + bottom = 4.dp + ) + } else { + Modifier.padding(end = 8.dp) + } ) - } else { - Modifier.padding(end = 8.dp) - } + ) + } + } + + is DrawerChannelIconType.Painter -> { + Icon( + painter = iconType.painter, + contentDescription = null, + tint = LocalContentColor.current, + modifier = Modifier + .padding(end = 8.dp) + .size(32.dp) + .padding(4.dp) ) - ) + } } Text( diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 4663c3fc..7c6625ad 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -94,6 +94,7 @@ import chat.revolt.internals.Changelogs import chat.revolt.ndk.Pipebomb import chat.revolt.persistence.KVStorage import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog +import chat.revolt.screens.chat.views.FriendsScreen import chat.revolt.screens.chat.views.HomeScreen import chat.revolt.screens.chat.views.NoCurrentChannelScreen import chat.revolt.screens.chat.views.channel.ChannelScreen @@ -112,9 +113,9 @@ import com.airbnb.lottie.compose.rememberLottieComposition import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import io.sentry.Sentry -import javax.inject.Inject import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel @SuppressLint("StaticFieldLeak") @@ -304,7 +305,7 @@ fun ChatRouterScreen( var useTabletAwareUI by remember { mutableStateOf(false) } - val drawerBackHandler = remember { + val toggleDrawerLda = remember { { scope.launch { if (drawerState.isOpen) { @@ -376,7 +377,7 @@ fun ChatRouterScreen( .distinctUntilChanged() .collect { sizeClass -> useTabletAwareUI = sizeClass.widthSizeClass == WindowWidthSizeClass.Expanded && - sizeClass.heightSizeClass != WindowHeightSizeClass.Compact + sizeClass.heightSizeClass != WindowHeightSizeClass.Compact } } @@ -680,8 +681,8 @@ fun ChatRouterScreen( navController = navController, topNav = topNav, useDrawer = false, - drawerBackHandler = { - drawerBackHandler() + toggleDrawer = { + toggleDrawerLda() }, onShowUserContextSheet = { target, server -> userContextSheetTarget = target @@ -720,8 +721,8 @@ fun ChatRouterScreen( navController = navController, topNav = topNav, useDrawer = true, - drawerBackHandler = { - drawerBackHandler() + toggleDrawer = { + toggleDrawerLda() }, drawerState = drawerState, onShowUserContextSheet = { target, server -> @@ -857,21 +858,21 @@ fun Sidebar( // - Add the servers that aren't in the ordering to the end of the list. // - Sort the servers that aren't in the ordering by their ID (creation order). ( - ( - RevoltAPI.serverCache.values.filter { - SyncedSettings.ordering.servers.contains( - it.id - ) - } - .sortedBy { SyncedSettings.ordering.servers.indexOf(it.id) } - ) + ( - RevoltAPI.serverCache.values.filter { - !SyncedSettings.ordering.servers.contains( - it.id - ) - }.sortedBy { it.id } + ( + RevoltAPI.serverCache.values.filter { + SyncedSettings.ordering.servers.contains( + it.id + ) + } + .sortedBy { SyncedSettings.ordering.servers.indexOf(it.id) } + ) + ( + RevoltAPI.serverCache.values.filter { + !SyncedSettings.ordering.servers.contains( + it.id + ) + }.sortedBy { it.id } + ) ) - ) .forEach { server -> if (server.id == null || server.name == null) return@forEach @@ -935,7 +936,7 @@ fun ChannelNavigator( navController: NavHostController, topNav: NavController, useDrawer: Boolean, - drawerBackHandler: () -> Unit, + toggleDrawer: () -> Unit, drawerState: DrawerState? = null, onShowUserContextSheet: (String, String?) -> Unit ) { @@ -945,14 +946,28 @@ fun ChannelNavigator( NavHost(navController = navController, startDestination = "home") { composable("home") { BackHandler(enabled = useDrawer) { - drawerBackHandler() + toggleDrawer() } - HomeScreen(navController = topNav) + HomeScreen( + navController = topNav, + useDrawer = useDrawer, + onDrawerClicked = toggleDrawer, + ) + } + + composable("friends") { + BackHandler(enabled = useDrawer) { + toggleDrawer() + } + FriendsScreen( + useDrawer = useDrawer, + onDrawerClicked = toggleDrawer, + ) } composable("channel/{channelId}") { backStackEntry -> BackHandler(enabled = useDrawer) { - drawerBackHandler() + toggleDrawer() } val channelId = backStackEntry.arguments?.getString("channelId") @@ -979,7 +994,7 @@ fun ChannelNavigator( composable("no_current_channel") { BackHandler(enabled = useDrawer) { - drawerBackHandler() + toggleDrawer() } NoCurrentChannelScreen() diff --git a/app/src/main/java/chat/revolt/screens/chat/views/FriendsScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/FriendsScreen.kt new file mode 100644 index 00000000..ae0792b4 --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/chat/views/FriendsScreen.kt @@ -0,0 +1,276 @@ +package chat.revolt.screens.chat.views + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import chat.revolt.R +import chat.revolt.api.internals.FriendRequests +import chat.revolt.api.routes.user.unfriendUser +import chat.revolt.api.schemas.User +import chat.revolt.callbacks.Action +import chat.revolt.callbacks.ActionChannel +import chat.revolt.components.generic.PageHeader +import chat.revolt.components.generic.SheetClickable +import chat.revolt.components.generic.UserAvatar +import chat.revolt.components.generic.presenceFromStatus +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun FriendsOptionsSheet(onDenyAll: () -> Unit) { + SheetClickable( + icon = { modifier -> + Icon( + modifier = modifier, + painter = painterResource(R.drawable.ic_account_cancel_24dp), + contentDescription = null + ) + }, + label = { style -> + Text( + text = stringResource(R.string.friends_deny_all_incoming), + style = style + ) + }, + onClick = { onDenyAll() } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun FriendsScreen(useDrawer: Boolean, onDrawerClicked: () -> Unit) { + var optionsSheetShown by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + if (optionsSheetShown) { + val sheetState = rememberModalBottomSheetState() + + ModalBottomSheet( + onDismissRequest = { + optionsSheetShown = false + }, + sheetState = sheetState + ) { + FriendsOptionsSheet( + onDenyAll = { + scope.launch { + sheetState.hide() + } + with(Dispatchers.IO) { + scope.launch { + FriendRequests.getIncoming() + .forEach { it.id?.let { id -> unfriendUser(id) } } + } + } + } + ) + } + } + + Column { + PageHeader( + text = "Friends", + startButtons = { + if (useDrawer) { + IconButton(onClick = onDrawerClicked) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = stringResource(R.string.menu) + ) + } + } + }, + additionalButtons = { + IconButton(onClick = { + optionsSheetShown = true + }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.menu) + ) + } + } + ) + + LazyColumn { + stickyHeader(key = "incoming") { + Text( + text = AnnotatedString.Builder().apply { + pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) + append(stringResource(id = R.string.friends_incoming_requests)) + pop() + + pushStyle( + SpanStyle( + fontWeight = FontWeight.Medium, + fontSize = LocalTextStyle.current.fontSize * 0.8, + color = LocalContentColor.current.copy(alpha = 0.6f) + ) + ) + append("—${FriendRequests.getIncoming().size}") + pop() + }.toAnnotatedString(), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + ) + } + + items(FriendRequests.getIncoming().size) { + val item = FriendRequests.getIncoming()[it] + UserItem(item, onClick = { + scope.launch { + item.id?.let { userId -> + ActionChannel.send(Action.OpenUserSheet(userId, null)) + } + } + }) + } + + stickyHeader(key = "outgoing") { + Text( + text = AnnotatedString.Builder().apply { + pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) + append(stringResource(id = R.string.friends_outgoing_requests)) + pop() + + pushStyle( + SpanStyle( + fontWeight = FontWeight.Medium, + fontSize = LocalTextStyle.current.fontSize * 0.8, + color = LocalContentColor.current.copy(alpha = 0.6f) + ) + ) + append("—${FriendRequests.getOutgoing().size}") + pop() + }.toAnnotatedString(), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + ) + } + + items(FriendRequests.getOutgoing().size) { + val item = FriendRequests.getOutgoing()[it] + UserItem(item, onClick = { + scope.launch { + item.id?.let { userId -> + ActionChannel.send(Action.OpenUserSheet(userId, null)) + } + } + }) + } + + stickyHeader(key = "blocked") { + Text( + text = AnnotatedString.Builder().apply { + pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) + append(stringResource(id = R.string.friends_blocked)) + pop() + + pushStyle( + SpanStyle( + fontWeight = FontWeight.Medium, + fontSize = LocalTextStyle.current.fontSize * 0.8, + color = LocalContentColor.current.copy(alpha = 0.6f) + ) + ) + append("—${FriendRequests.getBlocked().size}") + pop() + }.toAnnotatedString(), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + ) + } + + + items(FriendRequests.getBlocked().size) { + val item = FriendRequests.getBlocked()[it] + UserItem(item, onClick = { + scope.launch { + item.id?.let { userId -> + ActionChannel.send(Action.OpenUserSheet(userId, null)) + } + } + }) + } + } + } +} + +@Composable +fun UserItem(user: User, onClick: () -> Unit = {}) { + Row( + modifier = Modifier + .clickable { + onClick() + } + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + UserAvatar( + username = user.displayName + ?: user.username + ?: user.id!!, + avatar = user.avatar, + userId = user.id!!, + presence = presenceFromStatus( + user.status?.presence, + user.online ?: false + ) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = user.displayName + ?: user.username + ?: user.id, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt index f32de4ec..46c6a5e6 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt @@ -14,8 +14,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Star import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -33,7 +35,7 @@ import chat.revolt.components.generic.PageHeader import chat.revolt.components.screens.home.LinkOnHome @Composable -fun HomeScreen(navController: NavController) { +fun HomeScreen(navController: NavController, useDrawer: Boolean, onDrawerClicked: () -> Unit) { val context = LocalContext.current val catTransition = rememberInfiniteTransition(label = "cat") @@ -50,7 +52,19 @@ fun HomeScreen(navController: NavController) { Column( modifier = Modifier.safeDrawingPadding() ) { - PageHeader(text = stringResource(id = R.string.home)) + PageHeader( + text = stringResource(id = R.string.home), + startButtons = { + if (useDrawer) { + IconButton(onClick = onDrawerClicked) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = stringResource(R.string.menu) + ) + } + } + } + ) Box( modifier = Modifier .weight(1f) diff --git a/app/src/main/res/drawable/ic_home_24dp.xml b/app/src/main/res/drawable/ic_home_24dp.xml new file mode 100644 index 00000000..a563ffbe --- /dev/null +++ b/app/src/main/res/drawable/ic_home_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 62088b36..dca624ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -123,6 +123,12 @@ Join Jenvolt Jenvolt is the developer-run space for all things Android app and more. Support, feedback go here. Maybe you will get to try out new features! 👀 + Friends + Incoming Requests + Outgoing Requests + Blocked + Clear all incoming requests + Add server Bit awkward.