mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Merge 8971c538e92a5fcfb92d72a4c50f092a192ff469 into e07a38f6344cbb4af15c0bec1c13c6a5da82bd81
This commit is contained in:
commit
e660da819d
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
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