Merge 8971c538e92a5fcfb92d72a4c50f092a192ff469 into e07a38f6344cbb4af15c0bec1c13c6a5da82bd81

This commit is contained in:
hafil 2026-08-01 19:14:42 +04:00 committed by GitHub
commit e660da819d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 208 additions and 116 deletions

View File

@ -1500,7 +1500,7 @@ class ChatViewModel(
}
// Reset nickname
val newNickname = "anon${Random.nextInt(1000, 9999)}"
val newNickname = "ritsu${Random.nextInt(1000, 9999)}"
state.setNickname(newNickname)
dataManager.saveNickname(newNickname)

View File

@ -37,7 +37,7 @@ class DataManager(private val context: Context) {
return if (savedNickname != null) {
savedNickname
} else {
val randomNickname = "anon${Random.nextInt(1000, 9999)}"
val randomNickname = "ritsu${Random.nextInt(1000, 9999)}"
saveNickname(randomNickname)
randomNickname
}

View File

@ -27,6 +27,7 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
@ -36,6 +37,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
@ -46,6 +48,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -401,48 +404,39 @@ fun MessageItem(
.padding(top = topSpacing),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
Box(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Top
) {
// Provide a small end padding for own private messages so overlay doesn't cover text
val endPad = if (message.isPrivate && message.sender == currentUserNickname) 16.dp else 0.dp
// Create a custom layout that combines selectable text with clickable nickname areas
MessageTextWithClickableNicknames(
message = message,
messages = messages,
currentUserNickname = currentUserNickname,
meshService = meshService,
mentionPeerIdentities = mentionPeerIdentities,
colorScheme = colorScheme,
timeFormatter = timeFormatter,
showSender = showSender,
onNicknameClick = onNicknameClick,
onMessageLongPress = onMessageLongPress,
onCancelTransfer = onCancelTransfer,
onImageClick = onImageClick,
modifier = Modifier
.weight(1f)
.padding(end = endPad)
)
}
// The content aligns itself (self bubbles right, received bubbles left), so this just
// hands it the full width to place within.
MessageTextWithClickableNicknames(
message = message,
messages = messages,
currentUserNickname = currentUserNickname,
meshService = meshService,
mentionPeerIdentities = mentionPeerIdentities,
colorScheme = colorScheme,
timeFormatter = timeFormatter,
showSender = showSender,
onNicknameClick = onNicknameClick,
onMessageLongPress = onMessageLongPress,
onCancelTransfer = onCancelTransfer,
onImageClick = onImageClick,
modifier = Modifier.fillMaxWidth()
)
// Delivery status for private messages (overlay, non-displacing)
if (message.isPrivate && message.sender == currentUserNickname) {
message.deliveryStatus?.let { status ->
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 2.dp)
) {
DeliveryStatusIcon(status = status)
}
// Delivery status for private messages: a small right-aligned marker beneath the bubble,
// where it no longer risks overlapping the right-aligned self bubble.
if (message.isPrivate && message.sender == currentUserNickname) {
message.deliveryStatus?.let { status ->
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 2.dp, end = 4.dp),
contentAlignment = Alignment.CenterEnd
) {
DeliveryStatusIcon(status = status)
}
}
}
// Link previews removed; links are now highlighted inline and clickable within the message text
}
}
@ -703,26 +697,8 @@ internal fun TextMessageLayout(
)
}
// The timestamp trails the body rather than occupying its own column, so a short message
// no longer reserves a full-width row for eight grey characters.
val bodyText = remember(
displayMessage,
currentUserNickname,
palette,
colorScheme.onSurface,
colorScheme.secondary,
mentionPeerIdentities,
timeFormatter
) {
formatTextMessageBody(
message = displayMessage,
currentUserNickname = currentUserNickname,
palette = palette,
contentColor = colorScheme.onSurface,
linkColor = colorScheme.secondary,
mentionPeerIdentities = mentionPeerIdentities,
timeFormatter = timeFormatter,
)
}
// no longer reserves a full-width row for eight grey characters. The body is built further
// below against the bubble's own content color (see resolvedBodyText).
val isSelf = message.isFromSelf(currentUserNickname, myPeerId)
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
@ -731,9 +707,49 @@ internal fun TextMessageLayout(
onMessageLongPress?.invoke(message)
}
// Bubble geometry: three rounded corners, with the corner on the speaker's own side tightened
// into a subtle "tail". Self bubbles sit on the right and tuck their bottom-right corner;
// received bubbles sit on the left and tuck their bottom-left corner.
val corner = ChatVisualTokens.BubbleCornerRadius
val tail = ChatVisualTokens.BubbleTailRadius
val bubbleShape = if (isSelf) {
RoundedCornerShape(topStart = corner, topEnd = corner, bottomEnd = tail, bottomStart = corner)
} else {
RoundedCornerShape(topStart = corner, topEnd = corner, bottomEnd = corner, bottomStart = tail)
}
val bubbleColor = if (isSelf) {
colorScheme.primaryContainer
} else {
colorScheme.surfaceVariant.copy(alpha = ChatVisualTokens.BubbleSurfaceAlpha)
}
val bubbleContentColor = if (isSelf) colorScheme.onPrimaryContainer else colorScheme.onSurface
// Rebuild the body against the bubble's own content color so self-message text stays legible
// on the tinted primary container rather than defaulting to the surface's onSurface tone.
val resolvedBodyText = remember(
displayMessage,
currentUserNickname,
palette,
bubbleContentColor,
colorScheme.secondary,
mentionPeerIdentities,
timeFormatter
) {
formatTextMessageBody(
message = displayMessage,
currentUserNickname = currentUserNickname,
palette = palette,
contentColor = bubbleContentColor,
linkColor = colorScheme.secondary,
mentionPeerIdentities = mentionPeerIdentities,
timeFormatter = timeFormatter,
)
}
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(MessageGrouping.SENDER_TO_BODY_SPACING),
horizontalAlignment = if (isSelf) Alignment.End else Alignment.Start,
) {
if (showSender) {
AnnotatedClickableText(
@ -750,8 +766,12 @@ internal fun TextMessageLayout(
},
onLongPress = handleLongPress,
modifier = Modifier
.fillMaxWidth()
.padding(top = MessageGrouping.SENDER_TOP_PADDING),
.padding(
top = MessageGrouping.SENDER_TOP_PADDING,
// Nudge the label off the bubble's rounded edge so it lines up with the text.
start = if (isSelf) 0.dp else ChatVisualTokens.BubblePaddingHorizontal,
end = if (isSelf) ChatVisualTokens.BubblePaddingHorizontal else 0.dp,
),
fontFamily = BitchatFontFamily,
softWrap = false,
overflow = TextOverflow.Ellipsis,
@ -759,32 +779,52 @@ internal fun TextMessageLayout(
)
}
AnnotatedClickableText(
text = bodyText,
annotationTags = listOf("geohash_click", "url_click"),
onAnnotationClick = { tag, item ->
when (tag) {
"geohash_click" -> {
navigateToGeohash(context, item)
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
true
}
// Constrain the bubble to a fraction of the available width so short messages hug their
// content while long ones still wrap instead of touching the opposite edge.
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val maxBubbleWidth = maxWidth * ChatVisualTokens.BubbleMaxWidthFraction
Box(modifier = Modifier.fillMaxWidth()) {
Surface(
color = bubbleColor,
contentColor = bubbleContentColor,
shape = bubbleShape,
modifier = Modifier
.align(if (isSelf) Alignment.CenterEnd else Alignment.CenterStart)
.widthIn(max = maxBubbleWidth)
) {
AnnotatedClickableText(
text = resolvedBodyText,
annotationTags = listOf("geohash_click", "url_click"),
onAnnotationClick = { tag, item ->
when (tag) {
"geohash_click" -> {
navigateToGeohash(context, item)
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
true
}
"url_click" -> {
openMessageUrl(context, item)
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
true
}
"url_click" -> {
openMessageUrl(context, item)
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
true
}
else -> false
else -> false
}
},
onLongPress = handleLongPress,
modifier = Modifier.padding(
horizontal = ChatVisualTokens.BubblePaddingHorizontal,
vertical = ChatVisualTokens.BubblePaddingVertical,
),
fontFamily = BitchatFontFamily,
softWrap = true,
overflow = TextOverflow.Visible,
style = MessageBodyTextStyle.copy(color = bubbleContentColor),
)
}
},
onLongPress = handleLongPress,
fontFamily = BitchatFontFamily,
softWrap = true,
overflow = TextOverflow.Visible,
style = MessageBodyTextStyle.copy(color = colorScheme.onSurface),
)
}
}
}
}

View File

@ -49,24 +49,28 @@ data class BitchatPalette(
)
val DarkBitchatPalette = BitchatPalette(
inputOutline = Color(0xFF333635),
inputOutlineFocused = Color(0xFF5A605D),
inputSurface = Color(0xFF0B0B0B),
inputSurfaceFocused = Color(0xFF151515),
inputButton = Color(0xFF1E1E1E),
textTertiary = Color(0xFF6B776B),
// Neutral M3 tones aligned to the new dark surfaces (surface 0xFF191C19,
// surfaceVariant/outlineVariant 0xFF43483F, outline 0xFF8D9287).
inputOutline = Color(0xFF43483F),
inputOutlineFocused = Color(0xFF8D9287),
inputSurface = Color(0xFF1E211D),
inputSurfaceFocused = Color(0xFF262A24),
inputButton = Color(0xFF2A2E28),
textTertiary = Color(0xFF8A8F84),
accentOrange = Color(0xFFFF9F0A),
accentPurple = Color(0xFFBF5AF2),
peerColors = PeerColorStyle.Dark,
)
val LightBitchatPalette = BitchatPalette(
inputOutline = Color(0xFFCFD3D1),
inputOutlineFocused = Color(0xFF8E9490),
inputSurface = Color(0xFFFAFAFA),
inputSurfaceFocused = Color(0xFFF2F2F2),
inputButton = Color(0xFFE8E8E8),
textTertiary = Color(0xFF757F75),
// Neutral M3 tones aligned to the new light surfaces (surface 0xFFF2F6F2,
// surfaceVariant 0xFFE7EDE7, outline 0xFFCBD6CB).
inputOutline = Color(0xFFCBD6CB),
inputOutlineFocused = Color(0xFF8E948E),
inputSurface = Color(0xFFF2F6F2),
inputSurfaceFocused = Color(0xFFE7EDE7),
inputButton = Color(0xFFE0E6E0),
textTertiary = Color(0xFF6C766C),
accentOrange = Color(0xFFFF9500),
accentPurple = Color(0xFFAF52DE),
peerColors = PeerColorStyle.Light,

View File

@ -37,6 +37,19 @@ internal object ChatVisualTokens {
val SenderTopPadding: Dp = 8.dp
val SenderToBodySpacing: Dp = 4.dp
// MARK: - Bubble geometry
/** Rounded corner used on the three "free" corners of a message bubble. */
val BubbleCornerRadius: Dp = 16.dp
/** Tightened corner on the speaker's side, giving the bubble a subtle tail. */
val BubbleTailRadius: Dp = 4.dp
/** Horizontal / vertical padding inside a bubble, around the text. */
val BubblePaddingHorizontal: Dp = 12.dp
val BubblePaddingVertical: Dp = 8.dp
/** A bubble never grows past this fraction of the list width, so long lines still wrap. */
const val BubbleMaxWidthFraction: Float = 0.82f
/** Background tint alpha for a received (non-self) bubble sitting on the chat surface. */
const val BubbleSurfaceAlpha: Float = 0.65f
const val SenderSuffixAlpha: Float = 0.60f
const val HighlightAlpha: Float = 0.20f
const val MutedTextAlpha: Float = 0.50f

View File

@ -0,0 +1,20 @@
package com.bitchat.android.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
/**
* Rounded Material 3 shape scale for the redesigned surface.
*
* There was previously no [Shapes] definition, so components fell back to Material's stock scale.
* Defining it here is what gives buttons, cards, text fields, chips, sheets, and the FAB their
* softer, modern corners app-wide from a single source no per-composable shape overrides needed.
*/
val BitchatShapes = Shapes(
extraSmall = RoundedCornerShape(6.dp),
small = RoundedCornerShape(10.dp),
medium = RoundedCornerShape(14.dp),
large = RoundedCornerShape(20.dp),
extraLarge = RoundedCornerShape(28.dp),
)

View File

@ -7,6 +7,8 @@ import android.view.WindowInsetsController
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@ -15,31 +17,35 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
// Standard UI semantics live in Material so stock components and custom Bitchat composables
// share one source of truth. LocalBitchatPalette below only supplies app-specific extra colors.
// Fixed fallback schemes for devices below Android 12 (no Material You dynamic color).
// Neutral Material 3 surfaces with a retained green-family accent so the app keeps its identity
// without the old terminal green-on-black surface tint.
internal val DarkBitchatColorScheme = darkColorScheme(
primary = Color(0xFF32D74B),
onPrimary = Color.Black,
primaryContainer = Color(0xFF163D1D),
primary = Color(0xFF7CDC8A),
onPrimary = Color(0xFF00390F),
primaryContainer = Color(0xFF1C4A28),
onPrimaryContainer = Color(0xFFB8F5C1),
secondary = Color(0xFF0A84FF),
onSecondary = Color.Black,
secondaryContainer = Color(0xFF082E54),
onSecondaryContainer = Color(0xFFC2E0FF),
secondary = Color(0xFF9CCBFF),
onSecondary = Color(0xFF00325A),
secondaryContainer = Color(0xFF1B4870),
onSecondaryContainer = Color(0xFFD3E4FF),
tertiary = DarkBitchatPalette.accentOrange,
onTertiary = Color.Black,
background = Color(0xFF000000),
onBackground = Color(0xFFF5F5F5),
surface = Color(0xFF0E150E),
onSurface = Color(0xFFF5F5F5),
surfaceVariant = Color(0xFF182118),
onSurfaceVariant = Color(0xFF9AA69A),
outline = Color(0xFF2A3A2A),
outlineVariant = Color(0xFF1C271C),
error = Color(0xFFFF453A),
onError = Color.Black
background = Color(0xFF111311),
onBackground = Color(0xFFE3E3DE),
surface = Color(0xFF191C19),
onSurface = Color(0xFFE3E3DE),
surfaceVariant = Color(0xFF43483F),
onSurfaceVariant = Color(0xFFC3C8BC),
outline = Color(0xFF8D9287),
outlineVariant = Color(0xFF43483F),
error = Color(0xFFFFB4AB),
onError = Color(0xFF690005)
)
internal val LightBitchatColorScheme = lightColorScheme(
@ -82,7 +88,15 @@ fun BitchatTheme(
}
}
val colorScheme = if (shouldUseDark) DarkBitchatColorScheme else LightBitchatColorScheme
val context = LocalContext.current
// Material You: track the wallpaper on Android 12+, else fall back to the fixed modern schemes.
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
dynamicColor && shouldUseDark -> dynamicDarkColorScheme(context)
dynamicColor && !shouldUseDark -> dynamicLightColorScheme(context)
shouldUseDark -> DarkBitchatColorScheme
else -> LightBitchatColorScheme
}
val palette = if (shouldUseDark) DarkBitchatPalette else LightBitchatPalette
val view = LocalView.current
@ -110,6 +124,7 @@ fun BitchatTheme(
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
shapes = BitchatShapes,
content = content
)
}

View File

@ -6,5 +6,5 @@
android:height="108dp">
<path
android:pathData="M-0.2617076 -0.1145289H135.7284V135.5812H-0.2617076V-0.1145289Z"
android:fillColor="#000000" />
android:fillColor="#248A3D" />
</vector>