diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 0cb0fdc5..d58f68a7 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -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) diff --git a/app/src/main/java/com/bitchat/android/ui/DataManager.kt b/app/src/main/java/com/bitchat/android/ui/DataManager.kt index 8517ec15..f143bbf1 100644 --- a/app/src/main/java/com/bitchat/android/ui/DataManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/DataManager.kt @@ -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 } diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index 5ab113db..703a881f 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -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), - ) + } + } } } diff --git a/app/src/main/java/com/bitchat/android/ui/theme/BitchatPalette.kt b/app/src/main/java/com/bitchat/android/ui/theme/BitchatPalette.kt index 8dfb87b0..6a4d3fa9 100644 --- a/app/src/main/java/com/bitchat/android/ui/theme/BitchatPalette.kt +++ b/app/src/main/java/com/bitchat/android/ui/theme/BitchatPalette.kt @@ -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, diff --git a/app/src/main/java/com/bitchat/android/ui/theme/ChatVisualTokens.kt b/app/src/main/java/com/bitchat/android/ui/theme/ChatVisualTokens.kt index cdd4d418..9e51781a 100644 --- a/app/src/main/java/com/bitchat/android/ui/theme/ChatVisualTokens.kt +++ b/app/src/main/java/com/bitchat/android/ui/theme/ChatVisualTokens.kt @@ -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 diff --git a/app/src/main/java/com/bitchat/android/ui/theme/Shapes.kt b/app/src/main/java/com/bitchat/android/ui/theme/Shapes.kt new file mode 100644 index 00000000..87d455a7 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/ui/theme/Shapes.kt @@ -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), +) diff --git a/app/src/main/java/com/bitchat/android/ui/theme/Theme.kt b/app/src/main/java/com/bitchat/android/ui/theme/Theme.kt index f2c4b0b7..da4716a7 100644 --- a/app/src/main/java/com/bitchat/android/ui/theme/Theme.kt +++ b/app/src/main/java/com/bitchat/android/ui/theme/Theme.kt @@ -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 ) } diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index b63113ef..0d8b2393 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -6,5 +6,5 @@ android:height="108dp"> + android:fillColor="#248A3D" />