refactor: sidebar

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-08-01 01:38:19 +02:00
parent aa443d2acb
commit fcdaf0759d
11 changed files with 1052 additions and 1187 deletions

View File

@ -63,10 +63,10 @@ import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.chat.NativeMessageField import chat.revolt.components.chat.NativeMessageField
import chat.revolt.components.emoji.EmojiPicker import chat.revolt.components.emoji.EmojiPicker
import chat.revolt.components.generic.presenceFromStatus
import chat.revolt.components.screens.chat.AttachmentManager import chat.revolt.components.screens.chat.AttachmentManager
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel import chat.revolt.components.screens.chat.drawer.ChannelItem
import chat.revolt.components.screens.chat.drawer.server.DrawerChannelIconType import chat.revolt.components.screens.chat.drawer.ChannelItemIconType
import chat.revolt.components.screens.chat.drawer.DMOrGroupItem
import chat.revolt.persistence.KVStorage import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.views.channel.ChannelScreenActivePane import chat.revolt.screens.chat.views.channel.ChannelScreenActivePane
import chat.revolt.ui.theme.RevoltTheme import chat.revolt.ui.theme.RevoltTheme
@ -342,39 +342,29 @@ fun ShareTargetScreen(
items(filteredChannels.count()) { items(filteredChannels.count()) {
val channel = filteredChannels.elementAt(it) val channel = filteredChannels.elementAt(it)
DrawerChannel( when (channel.channelType) {
iconType = DrawerChannelIconType.Channel( ChannelType.Group, ChannelType.DirectMessage -> DMOrGroupItem(
channel.channelType ?: ChannelType.TextChannel channel = channel,
), partner = ChannelUtils.resolveDMPartner(channel)?.let { u ->
name = (if (channel.server != null) "${channel.name} (${RevoltAPI.serverCache[channel.server]?.name})" else channel.name) RevoltAPI.userCache[u]
?: ChannelUtils.resolveName(channel)
?: stringResource(R.string.unknown),
selected = selectedChannel == channel.id,
hasUnread = false,
onClick = {
selectedChannel = channel.id
},
dmPartnerIcon = ChannelUtils.resolveDMPartner(
channel
)?.let { u -> RevoltAPI.userCache[u] }?.avatar,
dmPartnerName = ChannelUtils.resolveName(
channel
),
dmPartnerStatus = ChannelUtils.resolveDMPartner(
channel
)
?.let { u -> RevoltAPI.userCache[u] }?.status?.presence?.let { p ->
presenceFromStatus(
p,
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(
channel
)]?.online ?: false
)
}, },
dmPartnerId = ChannelUtils.resolveDMPartner( isCurrent = selectedChannel == channel.id,
channel hasUnread = false,
), onDestinationChanged = { selectedChannel = channel.id },
) onOpenChannelContextSheet = {}
)
else -> ChannelItem(
iconType = ChannelItemIconType.Channel(
channel.channelType ?: ChannelType.TextChannel
),
channel = channel,
isCurrent = selectedChannel == channel.id,
onDestinationChanged = { selectedChannel = channel.id },
onOpenChannelContextSheet = {},
appendServerName = true
)
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }

View File

@ -1,8 +1,6 @@
package chat.revolt.components.generic package chat.revolt.components.generic
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -15,31 +13,16 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
private val NoopHandler = {}
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun IconPlaceholder( fun IconPlaceholder(
name: String, name: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit = NoopHandler,
onLongClick: () -> Unit = NoopHandler,
fontSize: TextUnit = 20.sp fontSize: TextUnit = 20.sp
) { ) {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = modifier modifier = modifier
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
.then(
if (onClick != NoopHandler || onLongClick != NoopHandler) {
Modifier.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
} else {
Modifier
}
)
) { ) {
Text( Text(
text = name.first().uppercase(), text = name.first().uppercase(),

File diff suppressed because it is too large Load Diff

View File

@ -1,623 +0,0 @@
package chat.revolt.components.screens.chat.drawer.channel
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.widget.AppCompatImageView
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
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.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.surfaceColorAtElevation
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import chat.revolt.R
import chat.revolt.activities.RevoltTweenColour
import chat.revolt.activities.RevoltTweenDp
import chat.revolt.activities.RevoltTweenFloat
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.CategorisedChannelList
import chat.revolt.api.internals.ChannelUtils
import chat.revolt.api.routes.user.openDM
import chat.revolt.api.schemas.ChannelType
import chat.revolt.api.schemas.ServerFlags
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.screens.chat.ChatRouterDestination
import chat.revolt.sheets.ChannelContextSheet
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import kotlinx.coroutines.launch
import kotlin.math.max
const val BANNER_HEIGHT_COMPACT = 56
const val BANNER_HEIGHT_EXPANDED = 128
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun RowScope.ChannelList(
serverId: String?,
currentDestination: ChatRouterDestination,
onDestinationChange: (ChatRouterDestination) -> Unit,
onServerSheetOpenFor: (String) -> Unit
) {
val lazyListState = rememberLazyListState()
val enableSmallBanner by remember {
derivedStateOf {
lazyListState.firstVisibleItemScrollOffset > 40 ||
lazyListState.firstVisibleItemIndex > 0
}
}
val bannerHeight by animateDpAsState(
targetValue = if (enableSmallBanner) BANNER_HEIGHT_COMPACT.dp else BANNER_HEIGHT_EXPANDED.dp,
animationSpec = RevoltTweenDp,
label = "Banner Height"
)
val bannerImageOpacity by animateFloatAsState(
targetValue = if (enableSmallBanner) 0f else 1f,
animationSpec = RevoltTweenFloat,
label = "Banner Image Opacity"
)
val bannerTextColour by animateColorAsState(
targetValue = if (enableSmallBanner) LocalContentColor.current else Color.White,
animationSpec = RevoltTweenColour,
label = "Banner Text Colour"
)
var channelContextSheetShown by remember { mutableStateOf(false) }
var channelContextSheetTarget by remember { mutableStateOf("") }
if (channelContextSheetShown) {
val channelContextSheetState = rememberModalBottomSheetState()
ModalBottomSheet(
sheetState = channelContextSheetState,
onDismissRequest = {
channelContextSheetShown = false
}
) {
ChannelContextSheet(
channelId = channelContextSheetTarget,
onHideSheet = {
channelContextSheetState.hide()
channelContextSheetShown = false
}
)
}
}
val dmAbleChannels =
RevoltAPI.channelCache.values
.filter { it.channelType == ChannelType.DirectMessage || it.channelType == ChannelType.Group }
.filter { if (it.channelType == ChannelType.DirectMessage) it.active == true else true }
.sortedBy { it.lastMessageID ?: it.id }
.reversed()
val server = RevoltAPI.serverCache[serverId]
val categorisedChannels = server?.let {
ChannelUtils.categoriseServerFlat(it)
}
val scope = rememberCoroutineScope()
Surface(
tonalElevation = 1.dp,
modifier = Modifier
.padding(start = 4.dp, top = 8.dp, bottom = 8.dp)
.clip(RoundedCornerShape(16.dp))
.fillMaxWidth()
) {
LazyColumn(
Modifier
.weight(1f)
.fillMaxSize(),
state = lazyListState
) {
if (serverId == null) {
stickyHeader(
key = "header"
) {
Box(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 0.dp, bottom = 8.dp)
.alpha(0.9f)
.height(BANNER_HEIGHT_COMPACT.dp + 8.dp) // due to padding in Text
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
.weight(1f)
) {
Text(
text = stringResource(R.string.direct_messages),
style = MaterialTheme.typography.labelLarge,
fontSize = 16.sp,
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 16.dp)
)
}
}
item(
key = "home"
) {
DrawerChannel(
name = stringResource(R.string.home),
iconType = DrawerChannelIconType.Painter(painterResource(R.drawable.ic_home_24dp)),
selected = currentDestination == ChatRouterDestination.Home,
hasUnread = false,
onClick = {
onDestinationChange(ChatRouterDestination.Home)
},
large = true
)
}
item(
key = "friends"
) {
DrawerChannel(
name = stringResource(R.string.friends),
iconType = DrawerChannelIconType.Painter(painterResource(R.drawable.ic_human_greeting_variant_24dp)),
selected = currentDestination == ChatRouterDestination.Friends,
hasUnread = false,
onClick = {
onDestinationChange(ChatRouterDestination.Friends)
},
large = true
)
}
item(
key = "notes"
) {
val notesChannelId =
RevoltAPI.channelCache.values.firstOrNull { it.channelType == ChannelType.SavedMessages }?.id
DrawerChannel(
name = stringResource(R.string.channel_notes),
iconType = DrawerChannelIconType.Channel(ChannelType.SavedMessages),
selected = currentDestination == ChatRouterDestination.Channel(
notesChannelId ?: ""
),
hasUnread = false,
onClick = {
if (notesChannelId != null) {
onDestinationChange(ChatRouterDestination.Channel(notesChannelId))
return@DrawerChannel
}
scope.launch {
val notesChannel = openDM(RevoltAPI.selfId ?: return@launch)
if (notesChannel.id != null) {
if (RevoltAPI.channelCache[notesChannel.id] == null)
RevoltAPI.channelCache[notesChannel.id] = notesChannel
}
onDestinationChange(
ChatRouterDestination.Channel(
notesChannel.id ?: return@launch
)
)
}
},
large = true
)
}
item(
key = "divider"
) {
Surface(
Modifier
.padding(vertical = 8.dp)
.fillMaxWidth()
.height(1.dp),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
) {}
}
items(
dmAbleChannels.size,
key = { index ->
val channel = dmAbleChannels.getOrNull(index)
channel?.id ?: index
}
) {
val channel = dmAbleChannels.getOrNull(it) ?: return@items
val partner =
if (channel.channelType == ChannelType.DirectMessage) {
RevoltAPI.userCache[
ChannelUtils.resolveDMPartner(
channel
)
]
} else {
null
}
DrawerChannel(
name = partner?.let { p -> User.resolveDefaultName(p) } ?: channel.name
?: stringResource(R.string.unknown),
iconType = DrawerChannelIconType.Channel(
channel.channelType ?: ChannelType.TextChannel
),
selected = currentDestination == ChatRouterDestination.Channel(
channel.id ?: ""
),
hasUnread = channel.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread(
channel.id!!,
lastMessageID
)
} ?: false,
dmPartnerIcon = partner?.avatar ?: channel.icon,
dmPartnerId = partner?.id,
dmPartnerName = partner?.let { p -> User.resolveDefaultName(p) },
dmPartnerStatus = presenceFromStatus(
status = partner?.status?.presence,
online = partner?.online ?: false
),
onClick = {
onDestinationChange(
ChatRouterDestination.Channel(
channel.id ?: return@DrawerChannel
)
)
},
onLongClick = {
channelContextSheetTarget = channel.id ?: return@DrawerChannel
channelContextSheetShown = true
}
)
}
} else {
stickyHeader {
Box(
contentAlignment = Alignment.BottomStart,
modifier = Modifier
.then(
// if there is no banner, we change the design slightly.
// instead of there being a banner card we make a "classic"
// sticky header á la Google Messages
if (server?.banner != null) {
Modifier.padding(vertical = 8.dp, horizontal = 8.dp)
} else {
Modifier.padding(
start = 0.dp,
end = 8.dp,
top = 0.dp,
bottom = 0.dp
)
}
)
.fillMaxWidth()
) {
if (server?.banner != null) {
Box(modifier = Modifier.height(bannerHeight)) {
Box(
modifier = Modifier
.alpha(max(0.95f, bannerImageOpacity))
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surface)
)
// *** ANDROIDVIEW RATIONALE ***
// Compose w/ Glide looks super laggy when resizing, because
// it tries to refetch the image every time. (luckily from cache)
// This is a temporary workaround until Glide can be resized
// without refetching in Compose.
AndroidView(
factory = { ctx ->
AppCompatImageView(ctx).apply {
scaleType = ImageView.ScaleType.CENTER_CROP
Glide.with(this)
.load("$REVOLT_FILES/banners/${server.banner.id}")
.transition(
DrawableTransitionOptions.withCrossFade()
)
.into(this)
}
},
update = {
it.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
},
modifier = Modifier
.alpha(bannerImageOpacity)
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
)
Box(
modifier = Modifier
.alpha(bannerImageOpacity)
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(
Brush.verticalGradient(
listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.3f)
)
)
)
)
}
} else {
Box(
modifier = Modifier
.alpha(0.9f)
.height(
BANNER_HEIGHT_COMPACT.dp + 8.dp
) // due to padding in Text
.fillMaxWidth()
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
)
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Spacer(Modifier.width(16.dp))
if (server?.flags has ServerFlags.Official) {
Icon(
painter = painterResource(
id = R.drawable.ic_revolt_decagram_24dp
),
contentDescription = stringResource(
R.string.server_flag_official
),
tint = if (server?.banner != null) {
bannerTextColour
} else {
LocalContentColor.current
},
modifier = Modifier
.padding(end = 8.dp)
.size(24.dp)
)
}
if (server?.flags has ServerFlags.Verified) {
Icon(
painter = painterResource(
id = R.drawable.ic_check_decagram_24dp
),
contentDescription = stringResource(
R.string.server_flag_verified
),
tint = if (server?.banner != null) {
bannerTextColour
} else {
LocalContentColor.current
},
modifier = Modifier
.padding(end = 8.dp)
.size(24.dp)
)
}
Text(
text = (
server?.name
?: stringResource(R.string.unknown)
),
style = MaterialTheme.typography.labelLarge,
color = if (server?.banner != null) {
bannerTextColour
} else {
LocalContentColor.current
},
fontSize = 16.sp,
modifier = Modifier
.then(
if (server?.banner != null) {
Modifier.padding(
start = 0.dp,
end = 16.dp,
top = 16.dp,
bottom = 16.dp
)
} else {
Modifier.padding(
start = 0.dp,
end = 24.dp,
top = 16.dp,
bottom = 16.dp
)
}
)
.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
IconButton(onClick = {
onServerSheetOpenFor(serverId ?: return@IconButton)
}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(
id = R.string.settings
),
tint = if (server?.banner != null) {
bannerTextColour
} else {
LocalContentColor.current
}
)
}
}
}
}
if (categorisedChannels.isNullOrEmpty()) {
item {
Column(
Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.no_channels_heading),
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
fontSize = 24.sp,
modifier = Modifier.padding(bottom = 16.dp)
)
Text(
text = stringResource(R.string.no_channels_body),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
)
}
}
} else {
items(
categorisedChannels.size,
key = { index ->
val channel = categorisedChannels.getOrNull(index)
channel?.let {
when (it) {
is CategorisedChannelList.Channel -> it.channel.id
is CategorisedChannelList.Category -> it.category.id
}
} ?: index
}
) {
when (val item = categorisedChannels.getOrNull(it)) {
is CategorisedChannelList.Channel -> {
val channel = item.channel
val partner =
if (channel.channelType == ChannelType.DirectMessage) {
RevoltAPI.userCache[
ChannelUtils.resolveDMPartner(
channel
)
]
} else {
null
}
DrawerChannel(
name = partner?.let { p -> User.resolveDefaultName(p) }
?: channel.name
?: stringResource(R.string.unknown),
iconType = DrawerChannelIconType.Channel(
channel.channelType ?: ChannelType.TextChannel
),
selected = currentDestination == ChatRouterDestination.Channel(
channel.id ?: ""
),
hasUnread = channel.lastMessageID?.let { lastMessageID ->
RevoltAPI.unreads.hasUnread(
channel.id!!,
lastMessageID
)
} ?: false,
dmPartnerIcon = partner?.avatar ?: channel.icon,
dmPartnerId = partner?.id,
dmPartnerName = partner?.let { p ->
User.resolveDefaultName(
p
)
},
dmPartnerStatus = presenceFromStatus(
status = partner?.status?.presence,
online = partner?.online ?: false
),
onClick = {
onDestinationChange(
ChatRouterDestination.Channel(
channel.id ?: return@DrawerChannel
)
)
},
onLongClick = {
channelContextSheetTarget =
channel.id ?: return@DrawerChannel
channelContextSheetShown = true
}
)
}
is CategorisedChannelList.Category -> {
val category = item.category
Text(
text = category.title ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.labelLarge,
fontSize = 16.sp,
modifier = Modifier
.padding(
start = 16.dp,
end = 16.dp,
top = 24.dp,
bottom = 16.dp
)
)
}
else -> {}
}
}
}
}
}
}
}

View File

@ -1,169 +0,0 @@
package chat.revolt.components.screens.chat.drawer.server
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
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
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.schemas.ChannelType
import chat.revolt.components.generic.GroupIcon
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(
iconType: DrawerChannelIconType,
name: String,
selected: Boolean,
hasUnread: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit = {},
dmPartnerStatus: Presence? = null,
dmPartnerName: String? = null,
dmPartnerIcon: AutumnResource? = null,
dmPartnerId: String? = null,
large: Boolean = false
) {
val backgroundColor = animateColorAsState(
if (selected) {
MaterialTheme.colorScheme.background
} else {
Color.Transparent
},
animationSpec = spring(),
label = "Channel background colour"
)
val unreadDotOpacity = animateFloatAsState(
if (hasUnread) 1f else 0f,
animationSpec = spring(),
label = "Unread dot opacity"
)
val channelAlpha = animateFloatAsState(
if (hasUnread || selected) 1f else 0.8f,
animationSpec = spring(),
label = "Channel alpha"
)
Row(
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(backgroundColor.value)
.alpha(channelAlpha.value)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
.padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
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)
)
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)
}
)
)
}
}
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 = name,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
)
if (hasUnread) {
Box(
modifier = Modifier
.offset(x = (-8).dp)
.clip(CircleShape)
.background(LocalContentColor.current)
.alpha(unreadDotOpacity.value)
.size(8.dp)
)
} else {
Spacer(modifier = Modifier.size(8.dp))
}
}
}

View File

@ -1,78 +0,0 @@
package chat.revolt.components.screens.chat.drawer.server
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
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.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import chat.revolt.api.REVOLT_FILES
import chat.revolt.components.generic.IconPlaceholder
import chat.revolt.components.generic.RemoteImage
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DrawerServer(
iconId: String?,
serverName: String,
hasUnreads: Boolean,
onLongClick: () -> Unit,
onClick: () -> Unit
) {
val unreadIndicatorAlpha = animateFloatAsState(
if (hasUnreads) 1f else 0f,
animationSpec = spring(),
label = "Unread indicator alpha"
)
Box(
contentAlignment = Alignment.CenterStart
) {
if (iconId != null) {
RemoteImage(
url = "$REVOLT_FILES/icons/$iconId/server.png?max_side=256",
modifier = Modifier
.padding(8.dp)
.size(48.dp)
.clip(CircleShape)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
),
description = serverName
)
} else {
IconPlaceholder(
name = serverName,
onClick = onClick,
onLongClick = onLongClick,
modifier = Modifier
.padding(8.dp)
.size(48.dp)
.clip(CircleShape)
)
}
// Unread indicator
Box(
modifier = Modifier
.padding(8.dp)
.size(8.dp)
.offset(x = (-12).dp)
.clip(CircleShape)
.alpha(unreadIndicatorAlpha.value)
.background(LocalContentColor.current)
)
}
}

View File

@ -1,26 +0,0 @@
package chat.revolt.components.screens.chat.drawer.server
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun DrawerServerlikeIcon(onClick: () -> Unit, content: @Composable () -> Unit) {
IconButton(
onClick = onClick,
modifier = Modifier
.padding(8.dp)
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surface)
) {
content()
}
}

View File

@ -1,26 +0,0 @@
package chat.revolt.components.screens.chat.drawer.server
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ServerDrawerSeparator() {
Box(
Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
.height(1.dp)
.width(48.dp)
.background(
MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.1f
)
)
)
}

View File

@ -56,7 +56,7 @@ fun ServerOverview(server: Server) {
modifier = Modifier modifier = Modifier
.height(166.dp) .height(166.dp)
.fillMaxWidth(), .fillMaxWidth(),
contentScale = ContentScale.FillWidth contentScale = ContentScale.Crop
) )
Box( Box(

View File

@ -6,25 +6,15 @@ import android.view.accessibility.AccessibilityManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DismissibleDrawerSheet import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer import androidx.compose.material3.DismissibleNavigationDrawer
@ -49,7 +39,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -57,32 +46,23 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.ChannelUtils
import chat.revolt.api.internals.DirectMessages import chat.revolt.api.internals.DirectMessages
import chat.revolt.api.realtime.DisconnectionState import chat.revolt.api.realtime.DisconnectionState
import chat.revolt.api.realtime.RealtimeSocket import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.schemas.ChannelType
import chat.revolt.api.schemas.User
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.callbacks.Action import chat.revolt.callbacks.Action
import chat.revolt.callbacks.ActionChannel import chat.revolt.callbacks.ActionChannel
import chat.revolt.components.chat.DisconnectedNotice import chat.revolt.components.chat.DisconnectedNotice
import chat.revolt.components.generic.GroupIcon import chat.revolt.components.screens.chat.drawer.ChannelSideDrawer
import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.presenceFromStatus
import chat.revolt.components.screens.chat.drawer.channel.ChannelList
import chat.revolt.components.screens.chat.drawer.server.DrawerServer
import chat.revolt.components.screens.chat.drawer.server.DrawerServerlikeIcon
import chat.revolt.components.screens.chat.drawer.server.ServerDrawerSeparator
import chat.revolt.components.screens.voice.VoiceChannelOverlay import chat.revolt.components.screens.voice.VoiceChannelOverlay
import chat.revolt.internals.Changelogs import chat.revolt.internals.Changelogs
import chat.revolt.internals.extensions.BottomSheetInsets
import chat.revolt.internals.extensions.zero
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.dialogs.safety.ReportServerDialog import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog
@ -538,7 +518,8 @@ fun ChatRouterScreen(
sheetState = serverContextSheetState, sheetState = serverContextSheetState,
onDismissRequest = { onDismissRequest = {
showServerContextSheet = false showServerContextSheet = false
} },
windowInsets = BottomSheetInsets
) { ) {
ServerContextSheet( ServerContextSheet(
serverId = serverContextSheetTarget, serverId = serverContextSheetTarget,
@ -718,7 +699,7 @@ fun ChatRouterScreen(
Row { Row {
DismissibleDrawerSheet( DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent, drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.navigationBars windowInsets = WindowInsets.zero
) { ) {
Sidebar( Sidebar(
viewModel = viewModel, viewModel = viewModel,
@ -755,7 +736,7 @@ fun ChatRouterScreen(
drawerContent = { drawerContent = {
DismissibleDrawerSheet( DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent, drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.navigationBars windowInsets = WindowInsets.zero
) { ) {
Sidebar( Sidebar(
viewModel = viewModel, viewModel = viewModel,
@ -809,192 +790,19 @@ fun Sidebar(
showSettingsButton: Boolean, showSettingsButton: Boolean,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
) { ) {
val scope = rememberCoroutineScope() ChannelSideDrawer(
onDestinationChanged = viewModel::setSaveDestination,
Column(Modifier.fillMaxWidth()) { currentDestination = viewModel.currentDestination,
Row { currentServer = currentServer,
Column( drawerState = drawerState,
modifier = Modifier navigateToServer = viewModel::navigateToServer,
.fillMaxHeight() onLongPressAvatar = onShowStatusSheet,
.verticalScroll(rememberScrollState()), onShowServerContextSheet = onShowServerContextSheet,
horizontalAlignment = Alignment.CenterHorizontally showSettingsIcon = showSettingsButton,
) { onOpenSettings = onOpenSettings,
UserAvatar( topNav = topNav,
username = RevoltAPI.userCache[RevoltAPI.selfId]?.let { onShowAddServerSheet = onShowAddServerSheet
User.resolveDefaultName( )
it
)
}
?: "",
presence = presenceFromStatus(
RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence,
RevoltAPI.userCache[RevoltAPI.selfId]?.online ?: false
),
userId = RevoltAPI.selfId ?: "",
avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar,
size = 48.dp,
presenceSize = 16.dp,
onClick = {
viewModel.setSaveDestination(ChatRouterDestination.defaultForDMList)
},
onLongClick = onShowStatusSheet,
modifier = Modifier
.padding(8.dp)
.size(48.dp)
)
DirectMessages.unreadDMs().forEach {
when (it.channelType) {
ChannelType.Group -> GroupIcon(
name = it.name ?: "?",
size = 48.dp,
onClick = {
it.id?.let { id ->
viewModel.setSaveDestination(ChatRouterDestination.Channel(id))
}
},
icon = it.icon,
modifier = Modifier
.padding(8.dp)
.size(48.dp)
)
else -> {
val partner =
if (it.channelType == ChannelType.DirectMessage) {
RevoltAPI.userCache[
ChannelUtils.resolveDMPartner(
it
)
]
} else {
null
}
UserAvatar(
username = partner?.let { p ->
User.resolveDefaultName(
p
)
} ?: it.name ?: "?",
presence = presenceFromStatus(
partner?.status?.presence,
partner?.online ?: false
),
userId = partner?.id ?: it.id ?: "",
avatar = partner?.avatar ?: it.icon,
size = 48.dp,
presenceSize = 16.dp,
onClick = {
it.id?.let { id ->
viewModel.setSaveDestination(
ChatRouterDestination.Channel(
id
)
)
}
},
modifier = Modifier
.padding(8.dp)
.size(48.dp)
)
}
}
}
ServerDrawerSeparator()
// This seems to confuse the formatter, here's what it does:
// - Take the list of servers and filter them by the ones that are in the ordering.
// - Sort the servers that are in the ordering using the ordering.
// - 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 }
)
)
.forEach { server ->
if (server.id == null || server.name == null) return@forEach
DrawerServer(
iconId = server.icon?.id,
serverName = server.name,
hasUnreads = RevoltAPI.unreads.serverHasUnread(
server.id
),
onLongClick = {
onShowServerContextSheet(server.id)
}
) {
viewModel.navigateToServer(server.id)
}
}
DrawerServerlikeIcon(
onClick = onShowAddServerSheet
) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(id = R.string.server_plus_alt),
modifier = Modifier.padding(4.dp)
)
}
DrawerServerlikeIcon(
onClick = { topNav.navigate("discover") }
) {
Icon(
painter = painterResource(id = R.drawable.ic_compass_24dp),
contentDescription = stringResource(id = R.string.discover_alt),
modifier = Modifier.padding(4.dp)
)
}
if (showSettingsButton) {
DrawerServerlikeIcon(
onClick = { onOpenSettings() }
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(id = R.string.settings),
modifier = Modifier.padding(4.dp)
)
}
}
}
Crossfade(
targetState = currentServer,
label = "Channel List"
) {
ChannelList(
serverId = it,
currentDestination = viewModel.currentDestination,
onDestinationChange = { destination ->
viewModel.setSaveDestination(destination)
scope.launch {
drawerState?.close()
}
},
onServerSheetOpenFor = { target ->
onShowServerContextSheet(target)
}
)
}
}
}
} }
@Composable @Composable

View File

@ -295,6 +295,7 @@
<string name="message_context_sheet_actions_delete_confirmation_yes">Delete</string> <string name="message_context_sheet_actions_delete_confirmation_yes">Delete</string>
<string name="message_context_sheet_actions_delete_confirmation_no">Keep</string> <string name="message_context_sheet_actions_delete_confirmation_no">Keep</string>
<string name="channel_context_sheet_open">Open channel actions</string>
<string name="channel_context_sheet_actions_copy_id">Copy ID</string> <string name="channel_context_sheet_actions_copy_id">Copy ID</string>
<string name="channel_context_sheet_actions_copy_id_copied">Copied channel ID to clipboard</string> <string name="channel_context_sheet_actions_copy_id_copied">Copied channel ID to clipboard</string>
<string name="channel_context_sheet_actions_mark_read">Mark as read</string> <string name="channel_context_sheet_actions_mark_read">Mark as read</string>