feat: friends screen

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-10-30 02:18:26 +01:00
parent ac5148e82c
commit 55e8d184f4
11 changed files with 506 additions and 68 deletions

View File

@ -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<User> {
return RevoltAPI.userCache.values.filter { user ->
user.relationship == "Incoming"
}
}
fun getIncomingCount(): Int {
return getIncoming().size
}
fun getOutgoing(): List<User> {
return RevoltAPI.userCache.values.filter { user ->
user.relationship == "Outgoing"
}
}
fun getOutgoingCount(): Int {
return getOutgoing().size
}
fun getBlocked(): List<User> {
return RevoltAPI.userCache.values.filter { user ->
user.relationship == "Blocked"
}
}
fun getBlockedCount(): Int {
return getBlocked().size
}
}

View File

@ -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.ServerMemberLeaveFrame
import chat.revolt.api.realtime.frames.receivable.ServerMemberUpdateFrame import chat.revolt.api.realtime.frames.receivable.ServerMemberUpdateFrame
import chat.revolt.api.realtime.frames.receivable.ServerUpdateFrame 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.receivable.UserUpdateFrame
import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
import chat.revolt.api.realtime.frames.sendable.PingFrame import chat.revolt.api.realtime.frames.sendable.PingFrame
@ -262,6 +263,27 @@ object RealtimeSocket {
existing.mergeWithPartial(userUpdateFrame.data) 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" -> { "ChannelUpdate" -> {
val channelUpdateFrame = val channelUpdateFrame =
RevoltJson.decodeFromString(ChannelUpdateFrame.serializer(), rawFrame) RevoltJson.decodeFromString(ChannelUpdateFrame.serializer(), rawFrame)

View File

@ -7,8 +7,8 @@ import chat.revolt.api.RevoltJson
import io.ktor.client.request.delete import io.ktor.client.request.delete
import io.ktor.client.request.put import io.ktor.client.request.put
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import kotlin.collections.set
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlin.collections.set
suspend fun blockUser(userId: String) { suspend fun blockUser(userId: String) {
val response = RevoltHttp.put("/users/$userId/block") val response = RevoltHttp.put("/users/$userId/block")
@ -39,3 +39,18 @@ suspend fun unblockUser(userId: String) {
val user = RevoltAPI.userCache[userId] ?: return val user = RevoltAPI.userCache[userId] ?: return
RevoltAPI.userCache[userId] = user.copy(relationship = "None") 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")
}

View File

@ -27,6 +27,7 @@ fun PageHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showBackButton: Boolean = false, showBackButton: Boolean = false,
onBackButtonClicked: () -> Unit = {}, onBackButtonClicked: () -> Unit = {},
startButtons: @Composable () -> Unit = {},
additionalButtons: @Composable () -> Unit = {}, additionalButtons: @Composable () -> Unit = {},
maxLines: Int = Int.MAX_VALUE maxLines: Int = Int.MAX_VALUE
) { ) {
@ -42,6 +43,7 @@ fun PageHeader(
) )
} }
} }
startButtons()
Text( Text(
text = text, text = text,
maxLines = maxLines, maxLines = maxLines,

View File

@ -68,6 +68,7 @@ import chat.revolt.api.schemas.User
import chat.revolt.api.schemas.has import chat.revolt.api.schemas.has
import chat.revolt.components.generic.presenceFromStatus import chat.revolt.components.generic.presenceFromStatus
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel import chat.revolt.components.screens.chat.drawer.server.DrawerChannel
import chat.revolt.components.screens.chat.drawer.server.DrawerChannelIconType
import chat.revolt.sheets.ChannelContextSheet import chat.revolt.sheets.ChannelContextSheet
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@ -90,7 +91,7 @@ fun RowScope.ChannelList(
val enableSmallBanner by remember { val enableSmallBanner by remember {
derivedStateOf { derivedStateOf {
lazyListState.firstVisibleItemScrollOffset > 40 || lazyListState.firstVisibleItemScrollOffset > 40 ||
lazyListState.firstVisibleItemIndex > 0 lazyListState.firstVisibleItemIndex > 0
} }
} }
@ -186,7 +187,7 @@ fun RowScope.ChannelList(
) { ) {
DrawerChannel( DrawerChannel(
name = stringResource(R.string.home), name = stringResource(R.string.home),
channelType = ChannelType.TextChannel, iconType = DrawerChannelIconType.Painter(painterResource(R.drawable.ic_home_24dp)),
selected = currentDestination == "home", selected = currentDestination == "home",
hasUnread = false, hasUnread = false,
onClick = { 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( item(
key = "notes" key = "notes"
) { ) {
@ -204,7 +220,7 @@ fun RowScope.ChannelList(
DrawerChannel( DrawerChannel(
name = stringResource(R.string.channel_notes), name = stringResource(R.string.channel_notes),
channelType = ChannelType.SavedMessages, iconType = DrawerChannelIconType.Channel(ChannelType.SavedMessages),
selected = currentDestination == "channel/{channelId}" && currentChannel == notesChannelId, selected = currentDestination == "channel/{channelId}" && currentChannel == notesChannelId,
hasUnread = false, hasUnread = false,
onClick = { onClick = {
@ -248,8 +264,10 @@ fun RowScope.ChannelList(
DrawerChannel( DrawerChannel(
name = partner?.let { p -> User.resolveDefaultName(p) } ?: channel.name name = partner?.let { p -> User.resolveDefaultName(p) } ?: channel.name
?: stringResource(R.string.unknown), ?: stringResource(R.string.unknown),
channelType = channel.channelType ?: ChannelType.TextChannel, iconType = DrawerChannelIconType.Channel(
channel.channelType ?: ChannelType.TextChannel
),
selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id, selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id,
hasUnread = channel.lastMessageID?.let { lastMessageID -> hasUnread = channel.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread( RevoltAPI.unreads.hasUnread(
@ -408,9 +426,9 @@ fun RowScope.ChannelList(
Text( Text(
text = ( text = (
server?.name server?.name
?: stringResource(R.string.unknown) ?: stringResource(R.string.unknown)
), ),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = if (server?.banner != null) { color = if (server?.banner != null) {
bannerTextColour bannerTextColour
@ -513,7 +531,9 @@ fun RowScope.ChannelList(
name = partner?.let { p -> User.resolveDefaultName(p) } name = partner?.let { p -> User.resolveDefaultName(p) }
?: channel.name ?: channel.name
?: stringResource(R.string.unknown), ?: stringResource(R.string.unknown),
channelType = channel.channelType ?: ChannelType.TextChannel, iconType = DrawerChannelIconType.Channel(
channel.channelType ?: ChannelType.TextChannel
),
selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id, selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id,
hasUnread = channel.lastMessageID?.let { lastMessageID -> hasUnread = channel.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread( RevoltAPI.unreads.hasUnread(

View File

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.offset
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.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text 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.generic.UserAvatar
import chat.revolt.components.screens.chat.ChannelIcon 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun DrawerChannel( fun DrawerChannel(
channelType: ChannelType, iconType: DrawerChannelIconType,
name: String, name: String,
selected: Boolean, selected: Boolean,
hasUnread: Boolean, hasUnread: Boolean,
@ -84,39 +91,55 @@ fun DrawerChannel(
.padding(vertical = 8.dp, horizontal = 16.dp), .padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
when (channelType) { when (iconType) {
ChannelType.DirectMessage -> UserAvatar( is DrawerChannelIconType.Channel -> {
username = dmPartnerName ?: "", when (val channelType = iconType.type) {
avatar = dmPartnerIcon, ChannelType.DirectMessage -> UserAvatar(
userId = dmPartnerId ?: "", username = dmPartnerName ?: "",
presence = dmPartnerStatus, avatar = dmPartnerIcon,
size = 32.dp, userId = dmPartnerId ?: "",
presenceSize = 16.dp, presence = dmPartnerStatus,
modifier = Modifier.padding(end = 8.dp) size = 32.dp,
) presenceSize = 16.dp,
modifier = Modifier.padding(end = 8.dp)
)
ChannelType.Group -> GroupIcon( ChannelType.Group -> GroupIcon(
name = name, name = name,
icon = dmPartnerIcon, icon = dmPartnerIcon,
size = 32.dp, size = 32.dp,
modifier = Modifier.padding(end = 8.dp) modifier = Modifier.padding(end = 8.dp)
) )
else -> ChannelIcon( else -> ChannelIcon(
channelType = channelType, channelType = channelType,
modifier = Modifier.then( modifier = Modifier.then(
if (large) { if (large) {
Modifier.padding( Modifier.padding(
end = 12.dp, end = 12.dp,
start = 4.dp, start = 4.dp,
top = 4.dp, top = 4.dp,
bottom = 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( Text(

View File

@ -94,6 +94,7 @@ import chat.revolt.internals.Changelogs
import chat.revolt.ndk.Pipebomb import chat.revolt.ndk.Pipebomb
import chat.revolt.persistence.KVStorage import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog 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.HomeScreen
import chat.revolt.screens.chat.views.NoCurrentChannelScreen import chat.revolt.screens.chat.views.NoCurrentChannelScreen
import chat.revolt.screens.chat.views.channel.ChannelScreen 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.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.sentry.Sentry import io.sentry.Sentry
import javax.inject.Inject
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel @HiltViewModel
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@ -304,7 +305,7 @@ fun ChatRouterScreen(
var useTabletAwareUI by remember { mutableStateOf(false) } var useTabletAwareUI by remember { mutableStateOf(false) }
val drawerBackHandler = remember { val toggleDrawerLda = remember {
{ {
scope.launch { scope.launch {
if (drawerState.isOpen) { if (drawerState.isOpen) {
@ -376,7 +377,7 @@ fun ChatRouterScreen(
.distinctUntilChanged() .distinctUntilChanged()
.collect { sizeClass -> .collect { sizeClass ->
useTabletAwareUI = sizeClass.widthSizeClass == WindowWidthSizeClass.Expanded && useTabletAwareUI = sizeClass.widthSizeClass == WindowWidthSizeClass.Expanded &&
sizeClass.heightSizeClass != WindowHeightSizeClass.Compact sizeClass.heightSizeClass != WindowHeightSizeClass.Compact
} }
} }
@ -680,8 +681,8 @@ fun ChatRouterScreen(
navController = navController, navController = navController,
topNav = topNav, topNav = topNav,
useDrawer = false, useDrawer = false,
drawerBackHandler = { toggleDrawer = {
drawerBackHandler() toggleDrawerLda()
}, },
onShowUserContextSheet = { target, server -> onShowUserContextSheet = { target, server ->
userContextSheetTarget = target userContextSheetTarget = target
@ -720,8 +721,8 @@ fun ChatRouterScreen(
navController = navController, navController = navController,
topNav = topNav, topNav = topNav,
useDrawer = true, useDrawer = true,
drawerBackHandler = { toggleDrawer = {
drawerBackHandler() toggleDrawerLda()
}, },
drawerState = drawerState, drawerState = drawerState,
onShowUserContextSheet = { target, server -> onShowUserContextSheet = { target, server ->
@ -857,21 +858,21 @@ fun Sidebar(
// - Add the servers that aren't in the ordering to the end of the list. // - 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). // - Sort the servers that aren't in the ordering by their ID (creation order).
( (
( (
RevoltAPI.serverCache.values.filter { RevoltAPI.serverCache.values.filter {
SyncedSettings.ordering.servers.contains( SyncedSettings.ordering.servers.contains(
it.id it.id
) )
} }
.sortedBy { SyncedSettings.ordering.servers.indexOf(it.id) } .sortedBy { SyncedSettings.ordering.servers.indexOf(it.id) }
) + ( ) + (
RevoltAPI.serverCache.values.filter { RevoltAPI.serverCache.values.filter {
!SyncedSettings.ordering.servers.contains( !SyncedSettings.ordering.servers.contains(
it.id it.id
) )
}.sortedBy { it.id } }.sortedBy { it.id }
)
) )
)
.forEach { server -> .forEach { server ->
if (server.id == null || server.name == null) return@forEach if (server.id == null || server.name == null) return@forEach
@ -935,7 +936,7 @@ fun ChannelNavigator(
navController: NavHostController, navController: NavHostController,
topNav: NavController, topNav: NavController,
useDrawer: Boolean, useDrawer: Boolean,
drawerBackHandler: () -> Unit, toggleDrawer: () -> Unit,
drawerState: DrawerState? = null, drawerState: DrawerState? = null,
onShowUserContextSheet: (String, String?) -> Unit onShowUserContextSheet: (String, String?) -> Unit
) { ) {
@ -945,14 +946,28 @@ fun ChannelNavigator(
NavHost(navController = navController, startDestination = "home") { NavHost(navController = navController, startDestination = "home") {
composable("home") { composable("home") {
BackHandler(enabled = useDrawer) { 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 -> composable("channel/{channelId}") { backStackEntry ->
BackHandler(enabled = useDrawer) { BackHandler(enabled = useDrawer) {
drawerBackHandler() toggleDrawer()
} }
val channelId = backStackEntry.arguments?.getString("channelId") val channelId = backStackEntry.arguments?.getString("channelId")
@ -979,7 +994,7 @@ fun ChannelNavigator(
composable("no_current_channel") { composable("no_current_channel") {
BackHandler(enabled = useDrawer) { BackHandler(enabled = useDrawer) {
drawerBackHandler() toggleDrawer()
} }
NoCurrentChannelScreen() NoCurrentChannelScreen()

View File

@ -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
)
}
}

View File

@ -14,8 +14,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -33,7 +35,7 @@ import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.home.LinkOnHome import chat.revolt.components.screens.home.LinkOnHome
@Composable @Composable
fun HomeScreen(navController: NavController) { fun HomeScreen(navController: NavController, useDrawer: Boolean, onDrawerClicked: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val catTransition = rememberInfiniteTransition(label = "cat") val catTransition = rememberInfiniteTransition(label = "cat")
@ -50,7 +52,19 @@ fun HomeScreen(navController: NavController) {
Column( Column(
modifier = Modifier.safeDrawingPadding() 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( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" />
</vector>

View File

@ -123,6 +123,12 @@
<string name="home_join_jenvolt">Join Jenvolt</string> <string name="home_join_jenvolt">Join Jenvolt</string>
<string name="home_join_jenvolt_description">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! 👀</string> <string name="home_join_jenvolt_description">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! 👀</string>
<string name="friends">Friends</string>
<string name="friends_incoming_requests">Incoming Requests</string>
<string name="friends_outgoing_requests">Outgoing Requests</string>
<string name="friends_blocked">Blocked</string>
<string name="friends_deny_all_incoming">Clear all incoming requests</string>
<string name="server_plus_alt">Add server</string> <string name="server_plus_alt">Add server</string>
<string name="no_channels_heading">Bit awkward.</string> <string name="no_channels_heading">Bit awkward.</string>