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.components.chat.NativeMessageField
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.drawer.server.DrawerChannel
import chat.revolt.components.screens.chat.drawer.server.DrawerChannelIconType
import chat.revolt.components.screens.chat.drawer.ChannelItem
import chat.revolt.components.screens.chat.drawer.ChannelItemIconType
import chat.revolt.components.screens.chat.drawer.DMOrGroupItem
import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.views.channel.ChannelScreenActivePane
import chat.revolt.ui.theme.RevoltTheme
@ -342,39 +342,29 @@ fun ShareTargetScreen(
items(filteredChannels.count()) {
val channel = filteredChannels.elementAt(it)
DrawerChannel(
iconType = DrawerChannelIconType.Channel(
channel.channelType ?: ChannelType.TextChannel
),
name = (if (channel.server != null) "${channel.name} (${RevoltAPI.serverCache[channel.server]?.name})" else channel.name)
?: 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
)
when (channel.channelType) {
ChannelType.Group, ChannelType.DirectMessage -> DMOrGroupItem(
channel = channel,
partner = ChannelUtils.resolveDMPartner(channel)?.let { u ->
RevoltAPI.userCache[u]
},
dmPartnerId = ChannelUtils.resolveDMPartner(
channel
),
)
isCurrent = selectedChannel == channel.id,
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))
}

View File

@ -1,8 +1,6 @@
package chat.revolt.components.generic
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
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.sp
private val NoopHandler = {}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun IconPlaceholder(
name: String,
modifier: Modifier = Modifier,
onClick: () -> Unit = NoopHandler,
onLongClick: () -> Unit = NoopHandler,
fontSize: TextUnit = 20.sp
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
.then(
if (onClick != NoopHandler || onLongClick != NoopHandler) {
Modifier.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
} else {
Modifier
}
)
) {
Text(
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
.height(166.dp)
.fillMaxWidth(),
contentScale = ContentScale.FillWidth
contentScale = ContentScale.Crop
)
Box(

View File

@ -6,25 +6,15 @@ import android.view.accessibility.AccessibilityManager
import android.view.inputmethod.InputMethodManager
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
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.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.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer
@ -49,7 +39,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.ChannelUtils
import chat.revolt.api.internals.DirectMessages
import chat.revolt.api.realtime.DisconnectionState
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.ActionChannel
import chat.revolt.components.chat.DisconnectedNotice
import chat.revolt.components.generic.GroupIcon
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.chat.drawer.ChannelSideDrawer
import chat.revolt.components.screens.voice.VoiceChannelOverlay
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.screens.chat.dialogs.safety.ReportMessageDialog
import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog
@ -538,7 +518,8 @@ fun ChatRouterScreen(
sheetState = serverContextSheetState,
onDismissRequest = {
showServerContextSheet = false
}
},
windowInsets = BottomSheetInsets
) {
ServerContextSheet(
serverId = serverContextSheetTarget,
@ -718,7 +699,7 @@ fun ChatRouterScreen(
Row {
DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.navigationBars
windowInsets = WindowInsets.zero
) {
Sidebar(
viewModel = viewModel,
@ -755,7 +736,7 @@ fun ChatRouterScreen(
drawerContent = {
DismissibleDrawerSheet(
drawerContainerColor = Color.Transparent,
windowInsets = WindowInsets.navigationBars
windowInsets = WindowInsets.zero
) {
Sidebar(
viewModel = viewModel,
@ -809,192 +790,19 @@ fun Sidebar(
showSettingsButton: Boolean,
onOpenSettings: () -> Unit,
) {
val scope = rememberCoroutineScope()
Column(Modifier.fillMaxWidth()) {
Row {
Column(
modifier = Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
UserAvatar(
username = RevoltAPI.userCache[RevoltAPI.selfId]?.let {
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)
}
)
}
}
}
ChannelSideDrawer(
onDestinationChanged = viewModel::setSaveDestination,
currentDestination = viewModel.currentDestination,
currentServer = currentServer,
drawerState = drawerState,
navigateToServer = viewModel::navigateToServer,
onLongPressAvatar = onShowStatusSheet,
onShowServerContextSheet = onShowServerContextSheet,
showSettingsIcon = showSettingsButton,
onOpenSettings = onOpenSettings,
topNav = topNav,
onShowAddServerSheet = onShowAddServerSheet
)
}
@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_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_copied">Copied channel ID to clipboard</string>
<string name="channel_context_sheet_actions_mark_read">Mark as read</string>