mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-12 05:07:56 +00:00
desktop: layered surfaces, reply line and rail tiles
This commit is contained in:
1 parent
6102e4e0ed
commit
8aa342061c
6 files changed
+139
-26
No files matched your search
@@ -447,10 +447,14 @@ private fun DesktopScreenLayout(userPickerState: MutableStateFlow<AnimatedViewSt
|
||||
DesktopRail(userPickerState, setPerformLA = AppLock::setPerformLA)
|
||||
}
|
||||
Box(Modifier.padding(start = railWidth).width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
StartPartOfScreen(userPickerState)
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA)
|
||||
@Composable
|
||||
fun StartPart() {
|
||||
StartPartOfScreen(userPickerState)
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA)
|
||||
}
|
||||
}
|
||||
if (LocalFlatLayout.current) FlatListSurface { StartPart() } else StartPart()
|
||||
}
|
||||
Box(Modifier.padding(start = railWidth).widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
|
||||
ModalManager.start.showInView()
|
||||
@@ -480,7 +484,9 @@ private fun DesktopScreenLayout(userPickerState: MutableStateFlow<AnimatedViewSt
|
||||
})
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = railWidth + DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
|
||||
if (!LocalFlatLayout.current) {
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier))
|
||||
}
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
@file:UseSerializers(UriSerializer::class, ComposeMessageSerializer::class)
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import chat.simplex.common.LocalFlatLayout
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -1763,7 +1764,7 @@ fun ComposeView(
|
||||
}
|
||||
|
||||
Surface(color = MaterialTheme.colors.background, contentColor = MaterialTheme.colors.onBackground) {
|
||||
Divider()
|
||||
if (!LocalFlatLayout.current) Divider()
|
||||
if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) {
|
||||
if (chat.chatInfo.groupInfo.businessChat == null) {
|
||||
val isChannel = chat.chatInfo.groupInfo.useRelays
|
||||
@@ -1838,7 +1839,7 @@ fun ComposeView(
|
||||
groupDirectInv = chat.chatInfo.contact.groupDirectInv
|
||||
)
|
||||
} else {
|
||||
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
|
||||
Row(Modifier.flatComposeField().padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
|
||||
AttachmentAndCommandsButtons()
|
||||
val broadcastPlaceholder = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.let { gi ->
|
||||
if (gi.useRelays && gi.membership.memberRole >= GroupMemberRole.Owner && chat.chatInfo.groupChatScope() == null) generalGetString(MR.strings.compose_view_broadcast)
|
||||
|
||||
+17
-1
@@ -23,6 +23,7 @@ import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.offset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.LocalFlatLayout
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.*
|
||||
@@ -42,6 +43,8 @@ private val FLAT_HEADER_TOP_PADDING = 8.dp
|
||||
// matches the horizontal text padding in CIMarkdownText
|
||||
val FLAT_TEXT_START_PADDING = 12.dp
|
||||
val FLAT_TEXT_VERTICAL_PADDING = 2.dp
|
||||
private val FLAT_ROW_INSET = 4.dp
|
||||
private val FLAT_ROW_CORNER = 6.dp
|
||||
private const val FLAT_SUBTLE_FILL_ALPHA = 0.04f
|
||||
private const val FLAT_HIGHLIGHT_ALPHA = 0.12f
|
||||
private const val FLAT_BORDER_ALPHA = 0.12f
|
||||
@@ -57,6 +60,9 @@ private const val FLAT_GROUP_SECONDS = 60
|
||||
val FLAT_META_BOTTOM_PADDING = 1.dp
|
||||
private val FLAT_QUOTE_BAR_WIDTH = 3.dp
|
||||
val FLAT_DELETED_FONT_SIZE = 14.sp
|
||||
val FLAT_REPLY_AVATAR_SIZE = 16.dp
|
||||
val FLAT_REPLY_ICON_SIZE = 14.dp
|
||||
val FLAT_REPLY_FONT_SIZE = 13.sp
|
||||
private const val FLAT_QUOTE_BAR_ALPHA = 0.25f
|
||||
private val FLAT_PILL_SHAPE = RoundedCornerShape(50)
|
||||
val FLAT_PILL_HORIZONTAL_PADDING = 6.dp
|
||||
@@ -146,7 +152,7 @@ fun FlatChatItemRow(
|
||||
hovered -> MaterialTheme.colors.onBackground.copy(alpha = FLAT_SUBTLE_FILL_ALPHA)
|
||||
else -> Color.Transparent
|
||||
}
|
||||
Box(modifier.fillMaxWidth().hoverable(interactionSource).background(background)) {
|
||||
Box(modifier.fillMaxWidth().padding(horizontal = FLAT_ROW_INSET).hoverable(interactionSource).background(background, RoundedCornerShape(FLAT_ROW_CORNER))) {
|
||||
Row(
|
||||
Modifier
|
||||
.heightIn(min = if (cItem.content.msgContent != null) FLAT_ROW_MIN_HEIGHT else 0.dp)
|
||||
@@ -410,3 +416,13 @@ private val SELECTION_CHECKBOX_SIZE = 22.dp
|
||||
fun flatSelectionTopPadding(showHeader: Boolean): Dp =
|
||||
if (showHeader) FLAT_HEADER_TOP_PADDING + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier - SELECTION_CHECKBOX_SIZE * fontSizeMultiplier) / 2
|
||||
else FLAT_TEXT_VERTICAL_PADDING + 1.dp
|
||||
|
||||
private val FLAT_COMPOSE_CORNER = 8.dp
|
||||
private const val FLAT_COMPOSE_FILL_ALPHA = 0.06f
|
||||
|
||||
@Composable
|
||||
fun Modifier.flatComposeField(): Modifier =
|
||||
if (!LocalFlatLayout.current) this
|
||||
else padding(horizontal = DEFAULT_PADDING_HALF * 1.5f, vertical = DEFAULT_PADDING_HALF)
|
||||
.clip(RoundedCornerShape(FLAT_COMPOSE_CORNER))
|
||||
.background(MaterialTheme.colors.onBackground.copy(alpha = FLAT_COMPOSE_FILL_ALPHA))
|
||||
+33
-3
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import chat.simplex.common.views.chat.group.MemberProfileImage
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
@@ -60,14 +61,14 @@ fun FramedItemView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean, stripLink: String? = null, prefix: AnnotatedString? = null) {
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean, stripLink: String? = null, prefix: AnnotatedString? = null, style: TextStyle = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface)) {
|
||||
MarkdownText(
|
||||
qi.text,
|
||||
qi.formattedText,
|
||||
toggleSecrets = true,
|
||||
maxLines = lines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface),
|
||||
style = style,
|
||||
linkMode = linkMode,
|
||||
uriHandler = if (appPlatform.isDesktop) uriHandler else null,
|
||||
showTimestamp = showTimestamp,
|
||||
@@ -220,6 +221,35 @@ fun FramedItemView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciReplyLine(qi: CIQuote) {
|
||||
Row(
|
||||
Modifier.padding(start = FLAT_TEXT_START_PADDING, top = FLAT_TEXT_VERTICAL_PADDING, end = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_reply), null, Modifier.size(FLAT_REPLY_ICON_SIZE), tint = MaterialTheme.colors.secondary)
|
||||
val quotedDir = qi.chatDir
|
||||
if (quotedDir is CIDirection.GroupRcv) {
|
||||
MemberProfileImage(FLAT_REPLY_AVATAR_SIZE, quotedDir.groupMember)
|
||||
}
|
||||
val sender = qi.sender(membership())
|
||||
if (sender != null) {
|
||||
Text(sender, fontSize = FLAT_REPLY_FONT_SIZE, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colors.secondary, maxLines = 1)
|
||||
}
|
||||
val replyStyle = TextStyle(fontSize = FLAT_REPLY_FONT_SIZE, color = MaterialTheme.colors.secondary)
|
||||
Box(Modifier.weight(1f, fill = false)) {
|
||||
when {
|
||||
qi.text.isNotEmpty() -> ciQuotedMsgTextView(qi, lines = 1, showTimestamp = false, style = replyStyle)
|
||||
qi.content is MsgContent.MCImage -> Text(stringResource(MR.strings.image_descr), style = replyStyle, maxLines = 1)
|
||||
qi.content is MsgContent.MCVideo -> Text(stringResource(MR.strings.video_descr), style = replyStyle, maxLines = 1)
|
||||
qi.content is MsgContent.MCVoice -> Text(stringResource(MR.strings.voice_message), style = replyStyle, maxLines = 1)
|
||||
qi.content is MsgContent.MCFile -> Text(stringResource(MR.strings.icon_descr_file), style = replyStyle, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta, chatTTL, showViaProxy, showTimestamp, showMenu, false, receiveFile)
|
||||
@@ -309,7 +339,7 @@ fun FramedItemView(
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
Header()
|
||||
ciQuoteView(ci.quotedItem)
|
||||
if (flat) ciReplyLine(ci.quotedItem) else ciQuoteView(ci.quotedItem)
|
||||
}
|
||||
} else {
|
||||
Header()
|
||||
|
||||
+69
-15
@@ -1,6 +1,10 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -11,6 +15,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -21,7 +26,6 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.ReactionIcon
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.localauth.VerticalDivider
|
||||
import chat.simplex.common.views.usersettings.SettingsView
|
||||
import chat.simplex.res.*
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
@@ -37,7 +41,31 @@ private val RAIL_ACTIVE_BACKGROUND_WIDTH = 40.dp
|
||||
private val RAIL_ACTIVE_BACKGROUND_HEIGHT = 32.dp
|
||||
private val RAIL_UNREAD_DOT_SIZE = 6.dp
|
||||
private const val RAIL_ACTIVE_BACKGROUND_ALPHA = 0.12f
|
||||
private const val RAIL_BACKGROUND_ALPHA = 0.04f
|
||||
private val RAIL_ITEM_INSET = 6.dp
|
||||
private const val RAIL_TILE_CORNER_PERCENT = 30
|
||||
private const val RAIL_TILE_CIRCLE_PERCENT = 50
|
||||
private const val RAIL_TILE_ALPHA = 0.15f
|
||||
private const val RAIL_MARK_ACTIVE_FRACTION = 0.8f
|
||||
private const val RAIL_MARK_HOVER_FRACTION = 0.4f
|
||||
// background darkening of the rail and the chat list; dark themes need more for a visible step
|
||||
private const val FLAT_RAIL_SHADE_LIGHT = 0.07f
|
||||
private const val FLAT_RAIL_SHADE_DARK = 0.4f
|
||||
private const val FLAT_LIST_SHADE_LIGHT = 0.035f
|
||||
private const val FLAT_LIST_SHADE_DARK = 0.2f
|
||||
|
||||
@Composable
|
||||
fun flatRailColor(): Color = flatShade(if (MaterialTheme.colors.isLight) FLAT_RAIL_SHADE_LIGHT else FLAT_RAIL_SHADE_DARK)
|
||||
|
||||
@Composable
|
||||
fun flatListColor(): Color = flatShade(if (MaterialTheme.colors.isLight) FLAT_LIST_SHADE_LIGHT else FLAT_LIST_SHADE_DARK)
|
||||
|
||||
@Composable
|
||||
private fun flatShade(amount: Float): Color = MaterialTheme.colors.background.mixWith(Color.Black, 1 - amount)
|
||||
|
||||
@Composable
|
||||
fun FlatListSurface(content: @Composable () -> Unit) {
|
||||
MaterialTheme(colors = MaterialTheme.colors.copy(background = flatListColor()), typography = MaterialTheme.typography, shapes = MaterialTheme.shapes, content = content)
|
||||
}
|
||||
|
||||
// Spec: spec/client/chat-list.md#DesktopRail
|
||||
@Composable
|
||||
@@ -46,10 +74,9 @@ fun DesktopRail(userPickerState: MutableStateFlow<AnimatedViewState>, setPerform
|
||||
Modifier
|
||||
.width(DESKTOP_RAIL_WIDTH * fontSizeSqrtMultiplier)
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colors.onBackground.copy(alpha = RAIL_BACKGROUND_ALPHA))
|
||||
.background(flatRailColor())
|
||||
) {
|
||||
RailContent(userPickerState, setPerformLA)
|
||||
VerticalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,20 +120,30 @@ private fun RailUsers(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
val switchEnabled = switchAllowed && (remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote)
|
||||
users.forEach { u ->
|
||||
val user = u.user
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
if (user.activeUser) {
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.size(RAIL_ACTIVE_MARK_WIDTH, RAIL_ITEM_SIZE * fontSizeSqrtMultiplier / 2)
|
||||
.background(MaterialTheme.colors.primary, RoundedCornerShape(topEnd = RAIL_ACTIVE_MARK_WIDTH, bottomEnd = RAIL_ACTIVE_MARK_WIDTH))
|
||||
)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val hovered = interactionSource.collectIsHoveredAsState().value
|
||||
val tileSize = RAIL_ITEM_SIZE * fontSizeSqrtMultiplier
|
||||
val markHeight by animateDpAsState(
|
||||
when {
|
||||
user.activeUser -> tileSize * RAIL_MARK_ACTIVE_FRACTION
|
||||
hovered -> tileSize * RAIL_MARK_HOVER_FRACTION
|
||||
else -> 0.dp
|
||||
}
|
||||
)
|
||||
val corner by animateIntAsState(if (user.activeUser || hovered) RAIL_TILE_CORNER_PERCENT else RAIL_TILE_CIRCLE_PERCENT)
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.size(RAIL_ACTIVE_MARK_WIDTH, markHeight)
|
||||
.background(MaterialTheme.colors.primary, RoundedCornerShape(topEnd = RAIL_ACTIVE_MARK_WIDTH, bottomEnd = RAIL_ACTIVE_MARK_WIDTH))
|
||||
)
|
||||
Box {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(enabled = if (user.activeUser) switchAllowed else switchEnabled) {
|
||||
.size(tileSize)
|
||||
.clip(RoundedCornerShape(percent = corner))
|
||||
.clickable(interactionSource = interactionSource, indication = LocalIndication.current, enabled = if (user.activeUser) switchAllowed else switchEnabled) {
|
||||
if (user.activeUser) {
|
||||
ModalManager.start.closeModals()
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
@@ -116,7 +153,7 @@ private fun RailUsers(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
}
|
||||
}
|
||||
) {
|
||||
ProfileImage(RAIL_ITEM_SIZE * fontSizeSqrtMultiplier, user.profile.image)
|
||||
RailProfileTile(user)
|
||||
}
|
||||
if (!user.activeUser && u.unreadCount > 0) {
|
||||
userUnreadBadge(u.unreadCount, user.showNtfs, hasPadding = false)
|
||||
@@ -126,6 +163,21 @@ private fun RailUsers(userPickerState: MutableStateFlow<AnimatedViewState>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RailProfileTile(user: User) {
|
||||
val image = user.profile.image
|
||||
if (image != null) {
|
||||
val bitmap = remember(image) { base64ToBitmap(image) }
|
||||
Image(bitmap, user.displayName, Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
|
||||
} else {
|
||||
val name = user.displayName
|
||||
val initial = name.take(if (name.firstOrNull()?.isHighSurrogate() == true) 2 else 1).uppercase()
|
||||
Box(Modifier.fillMaxSize().background(MaterialTheme.colors.primary.copy(alpha = RAIL_TILE_ALPHA)), contentAlignment = Alignment.Center) {
|
||||
Text(initial, color = MaterialTheme.colors.primary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RailTags() {
|
||||
val presetTags = remember { chatModel.presetTags }
|
||||
@@ -190,6 +242,8 @@ private fun RailItem(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = RAIL_ITEM_INSET)
|
||||
.clip(RoundedCornerShape(DEFAULT_PADDING_HALF))
|
||||
.combinedClickable(enabled = enabled, onClick = onClick, onLongClick = onLongClick)
|
||||
.onRightClick(onLongClick)
|
||||
.padding(vertical = 4.dp),
|
||||
|
||||
+7
-1
@@ -1,5 +1,7 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -25,6 +27,10 @@ object NoIndication : IndicationNodeFactory {
|
||||
override fun equals(other: Any?) = other === this
|
||||
}
|
||||
|
||||
private val FLAT_ROW_INSET = 6.dp
|
||||
private val FLAT_ROW_GAP = 1.dp
|
||||
private val FLAT_ROW_CORNER = 6.dp
|
||||
|
||||
@Composable
|
||||
actual fun ChatListNavLinkLayout(
|
||||
chatLinkPreview: @Composable () -> Unit,
|
||||
@@ -36,7 +42,7 @@ actual fun ChatListNavLinkLayout(
|
||||
nextChatSelected: State<Boolean>,
|
||||
) {
|
||||
val flat = LocalFlatLayout.current
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
var modifier = if (flat) Modifier.fillMaxWidth().padding(horizontal = FLAT_ROW_INSET, vertical = FLAT_ROW_GAP).clip(RoundedCornerShape(FLAT_ROW_CORNER)) else Modifier.fillMaxWidth()
|
||||
if (!disabled) modifier = modifier
|
||||
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true }
|
||||
|
||||
Reference in new issue
Block a user