mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Merge 19be706612528213c4943df7d3df6329a8a18828 into 945c9e025be1c3ea08db76398893c640452bcd20
This commit is contained in:
commit
bd097f0050
@ -47,6 +47,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
|
||||
@ -416,6 +417,39 @@ fun MessageItem(
|
||||
.padding(top = topSpacing),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||
) {
|
||||
// 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: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@ -767,13 +801,43 @@ 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. 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
|
||||
val handleLongPress: () -> Unit = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
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(
|
||||
// no longer reserves a full-width row for eight grey characters. Self bubbles leave the
|
||||
// timestamp to their meta cluster (see BubbleTextMessageLayout).
|
||||
val bodyText = remember(
|
||||
displayMessage,
|
||||
currentUserNickname,
|
||||
palette,
|
||||
colorScheme.onSurface,
|
||||
bubbleContentColor,
|
||||
colorScheme.secondary,
|
||||
mentionPeerIdentities,
|
||||
timeFormatter,
|
||||
@ -784,7 +848,7 @@ internal fun TextMessageLayout(
|
||||
message = displayMessage,
|
||||
currentUserNickname = currentUserNickname,
|
||||
palette = palette,
|
||||
contentColor = colorScheme.onSurface,
|
||||
contentColor = bubbleContentColor,
|
||||
linkColor = colorScheme.secondary,
|
||||
mentionPeerIdentities = mentionPeerIdentities,
|
||||
timeFormatter = timeFormatter,
|
||||
@ -810,6 +874,7 @@ internal fun TextMessageLayout(
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(MessageGrouping.SENDER_TO_BODY_SPACING),
|
||||
horizontalAlignment = if (isSelf) Alignment.End else Alignment.Start,
|
||||
) {
|
||||
if (showSender) {
|
||||
AnnotatedClickableText(
|
||||
@ -826,8 +891,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,
|
||||
@ -835,32 +904,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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
20
app/src/main/java/com/bitchat/android/ui/theme/Shapes.kt
Normal file
20
app/src/main/java/com/bitchat/android/ui/theme/Shapes.kt
Normal 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),
|
||||
)
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user