From e66ddde0635db47c95da871af2e7a831edccdb0f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:54:58 +0200 Subject: [PATCH] Bring private and group chat headers up to the main header's layout Both conversation headers were built on TopAppBar with a centred title, a back arrow on the left and everything else crowded into the title slot, at 14sp with 14dp icons. Moving between the timeline and a conversation visibly shifted the bar's height, insets and type. Introduces ConversationHeader, built from the main header's own tokens rather than TopAppBar: same ChatHeaderHeight, same 12/8dp edge insets, leading glyph in a 44dp slot so it lands exactly where the brand mark does, same -6dp optical nudge pulling the title toward it, same 17sp label. - Drops the back button; the close action on the right is the way out. Leaving a channel outright already lives on its row in the network sheet, so it does not need a second home beside the exit. - Leading glyph is the transport: globe over the internet, wifi/bluetooth/ routed on the mesh, matching the main header's channel button. - Actions are right-aligned and unweighted -- favourite, encryption state, close -- so a long title yields space to them instead of pushing them off screen. - Private chat titles use the primary green like every other header label, rather than orange for Nostr-reachable peers. Height and edge insets now belong to each header variant instead of the ChatFloatingHeader wrapper, which was applying them a second time to the channel header. Adds nine spec icons in the existing 20x20 / 1.25-stroke language -- bluetooth, wifi, routed, close, check, warning, sync, lock_open, envelope -- so the headers and peer rows no longer mix Material glyphs into the set. --- .../java/com/bitchat/android/ui/ChatHeader.kt | 201 +++++++++++++----- .../java/com/bitchat/android/ui/ChatScreen.kt | 58 +++-- .../com/bitchat/android/ui/ChatUIUtils.kt | 42 +++- .../com/bitchat/android/ui/InputComponents.kt | 139 ++++++------ .../bitchat/android/ui/MeshPeerListSheet.kt | 193 ++++++++--------- .../main/res/drawable/ic_spec_bluetooth.xml | 9 + app/src/main/res/drawable/ic_spec_check.xml | 7 + app/src/main/res/drawable/ic_spec_close.xml | 8 + .../main/res/drawable/ic_spec_envelope.xml | 8 + .../main/res/drawable/ic_spec_lock_open.xml | 13 ++ app/src/main/res/drawable/ic_spec_routed.xml | 11 + app/src/main/res/drawable/ic_spec_sync.xml | 10 + app/src/main/res/drawable/ic_spec_warning.xml | 9 + app/src/main/res/drawable/ic_spec_wifi.xml | 9 + app/src/main/res/values-ar/strings.xml | 2 - app/src/main/res/values-bn/strings.xml | 2 - app/src/main/res/values-de/strings.xml | 2 - app/src/main/res/values-es/strings.xml | 2 - app/src/main/res/values-fa/strings.xml | 2 - app/src/main/res/values-fil/strings.xml | 2 - app/src/main/res/values-fr/strings.xml | 2 - app/src/main/res/values-hi/strings.xml | 2 - app/src/main/res/values-id/strings.xml | 2 - app/src/main/res/values-it/strings.xml | 2 - app/src/main/res/values-ja/strings.xml | 2 - app/src/main/res/values-ka/strings.xml | 2 - app/src/main/res/values-ko/strings.xml | 2 - app/src/main/res/values-mg/strings.xml | 2 - app/src/main/res/values-ne/strings.xml | 2 - app/src/main/res/values-nl/strings.xml | 2 - app/src/main/res/values-pa-rPK/strings.xml | 2 - app/src/main/res/values-pt-rBR/strings.xml | 2 - app/src/main/res/values-pt/strings.xml | 2 - app/src/main/res/values-ru/strings.xml | 2 - app/src/main/res/values-sv/strings.xml | 2 - app/src/main/res/values-th/strings.xml | 2 - app/src/main/res/values-tr/strings.xml | 2 - app/src/main/res/values-ur/strings.xml | 2 - app/src/main/res/values-vi/strings.xml | 2 - app/src/main/res/values-zh/strings.xml | 2 - app/src/main/res/values/strings.xml | 2 - .../com/bitchat/android/ui/ChatUIUtilsTest.kt | 35 +++ 42 files changed, 482 insertions(+), 324 deletions(-) create mode 100644 app/src/main/res/drawable/ic_spec_bluetooth.xml create mode 100644 app/src/main/res/drawable/ic_spec_check.xml create mode 100644 app/src/main/res/drawable/ic_spec_close.xml create mode 100644 app/src/main/res/drawable/ic_spec_envelope.xml create mode 100644 app/src/main/res/drawable/ic_spec_lock_open.xml create mode 100644 app/src/main/res/drawable/ic_spec_routed.xml create mode 100644 app/src/main/res/drawable/ic_spec_sync.xml create mode 100644 app/src/main/res/drawable/ic_spec_warning.xml create mode 100644 app/src/main/res/drawable/ic_spec_wifi.xml diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index 8993b3ea..af38ea1d 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -42,6 +42,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bitchat.android.ui.theme.BitchatFontFamily +import androidx.annotation.DrawableRes +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.foundation.layout.RowScope import com.bitchat.android.R import com.bitchat.android.core.ui.component.button.BitChatBrandButton import com.bitchat.android.net.ArtiTorManager @@ -80,6 +83,15 @@ private val HeaderTapTarget = 44.dp /** Corner radius for the header's tappable label+icon clusters. */ private val HeaderClusterShape = RoundedCornerShape(8.dp) +/** + * Edge insets for the bar. + * + * Asymmetric because the leading glyph sits in a 44.dp tap target whose padding already supplies + * some optical inset, while the trailing action's does the same on the other side. + */ +internal val HeaderInsetStart = 12.dp +internal val HeaderInsetEnd = 8.dp + /** * A minimum-48x40 tap target wrapping a small icon. * @@ -273,39 +285,151 @@ fun NoiseSessionIcon( // The pre-redesign colours for the first two states were `0x87878700`, i.e. alpha 0x87 with // an all-but-transparent RGB - the icons were effectively invisible. They now use the // palette's secondary text colour. - val (icon, color, contentDescription) = when (sessionState) { + val (iconRes, color, contentDescription) = when (sessionState) { "uninitialized" -> Triple( - Icons.Outlined.NoEncryption, + R.drawable.ic_spec_lock_open, colorScheme.onSurfaceVariant, stringResource(R.string.cd_ready_for_handshake) ) "handshaking" -> Triple( - Icons.Outlined.Sync, + R.drawable.ic_spec_sync, colorScheme.onSurfaceVariant, stringResource(R.string.cd_handshake_in_progress) ) "established" -> Triple( - Icons.Filled.Lock, - palette.accentOrange, + R.drawable.ic_spec_lock, + colorScheme.primary, stringResource(R.string.cd_encrypted) ) else -> { // "failed" or any other state Triple( - Icons.Outlined.Warning, + R.drawable.ic_spec_warning, colorScheme.error, stringResource(R.string.cd_handshake_failed) ) } } - + Icon( - imageVector = icon, + painter = painterResource(iconRes), contentDescription = contentDescription, modifier = modifier, tint = color ) } +/** + * Reachability glyph for a conversation, drawn from the same spec set the main header uses. + * + * Mirrors the main header's channel button: a globe for anything reached over the internet, the + * range mark for the local mesh, and the more specific transport glyph when we know it. + */ +@DrawableRes +internal fun conversationTransportIcon( + isReachedOverInternet: Boolean, + isWifiAware: Boolean, + isDirect: Boolean +): Int = when { + isReachedOverInternet -> R.drawable.ic_spec_globe + isWifiAware -> R.drawable.ic_spec_wifi + isDirect -> R.drawable.ic_spec_bluetooth + else -> R.drawable.ic_spec_routed +} + +/** + * The shared chrome for a conversation header — private chats and channels alike. + * + * Deliberately built from the same tokens as [MainHeader] rather than from `TopAppBar`: identical + * height, identical 12/8.dp edge insets, the leading glyph in a [HeaderTapTarget]-sized slot so it + * lands exactly where the brand mark does, the same -6.dp optical nudge pulling the title toward + * that glyph, and the same [HeaderTextSize]. Anything less and the header visibly shifts as you + * move between the main timeline and a conversation. + * + * Actions are right-aligned and unweighted, so a long title yields space to them rather than + * pushing them off screen. + */ +@Composable +fun ConversationHeader( + @DrawableRes leadingIconRes: Int, + leadingIconTint: Color, + title: String, + modifier: Modifier = Modifier, + onTitleClick: (() -> Unit)? = null, + leadingContentDescription: String? = null, + actions: @Composable RowScope.() -> Unit = {} +) { + val colorScheme = MaterialTheme.colorScheme + + Row( + modifier = modifier + .fillMaxWidth() + .height(ChatHeaderHeight) + .padding(start = HeaderInsetStart, end = HeaderInsetEnd), + verticalAlignment = Alignment.CenterVertically + ) { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.size(HeaderTapTarget), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(leadingIconRes), + contentDescription = leadingContentDescription, + modifier = Modifier.size(HeaderIconSize), + tint = leadingIconTint + ) + } + + // Same optical correction as the main header: the 44.dp tap target leaves more gap + // than the design wants between glyph and label. + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + fontSize = HeaderTextSize, + fontWeight = FontWeight.Medium, + color = colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .offset(x = (-6).dp) + .then( + if (onTitleClick != null) { + Modifier + .clip(HeaderClusterShape) + .pressScaleClickable(onClick = onTitleClick) + .padding(horizontal = 6.dp, vertical = 4.dp) + } else { + Modifier + } + ) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + content = actions + ) + } +} + +/** An action slot in a [ConversationHeader], matching the main header's icon buttons. */ +@Composable +fun ConversationHeaderAction( + onClick: () -> Unit, + contentDescription: String?, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) = HeaderIconButton( + onClick = onClick, + contentDescription = contentDescription, + modifier = modifier, + content = content +) + @Composable fun NicknameEditor( value: String, @@ -486,51 +610,27 @@ private fun ChannelHeader( ) { val colorScheme = MaterialTheme.colorScheme - Box(modifier = Modifier.fillMaxWidth()) { - // Back: a chevron alone is unambiguous here and buys back ~40.dp of title space that - // the old "< back" label consumed. - HeaderIconButton( + // No back affordance: the close action on the right is the way out, exactly as in a private + // chat. Leaving the channel outright lives on its row in the network sheet, so it does not + // need a second, easily-mistaken home next to the exit. + ConversationHeader( + leadingIconRes = R.drawable.ic_spec_chat_bubbles, + leadingIconTint = colorScheme.primary, + leadingContentDescription = null, + title = "#$channel", + onTitleClick = onSidebarClick + ) { + ConversationHeaderAction( onClick = onBackClick, - contentDescription = stringResource(R.string.back), - modifier = Modifier.align(Alignment.CenterStart) + contentDescription = stringResource(R.string.close_plain) ) { Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back), + painter = painterResource(R.drawable.ic_spec_close), + contentDescription = stringResource(R.string.close_plain), modifier = Modifier.size(HeaderIconSize), - tint = colorScheme.primary + tint = colorScheme.onSurfaceVariant ) } - - // Title - perfectly centered regardless of other elements - Text( - text = "#$channel", - style = MaterialTheme.typography.titleMedium, - fontSize = HeaderTextSize, - color = colorScheme.primary, - modifier = Modifier - .align(Alignment.Center) - .clip(HeaderClusterShape) - .pressScaleClickable(onClick = onSidebarClick) - .heightIn(min = HeaderTapTarget) - .wrapContentHeight(Alignment.CenterVertically) - .padding(horizontal = 10.dp) - ) - - // Leave button - positioned on the right - Text( - text = stringResource(R.string.chat_leave), - style = MaterialTheme.typography.labelMedium, - fontSize = 15.sp, - color = colorScheme.error, - modifier = Modifier - .align(Alignment.CenterEnd) - .clip(HeaderClusterShape) - .pressScaleClickable(onClick = onLeaveChannel) - .heightIn(min = HeaderTapTarget) - .wrapContentHeight(Alignment.CenterVertically) - .padding(horizontal = 10.dp) - ) } } @@ -556,7 +656,10 @@ private fun MainHeader( val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle() Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .height(ChatHeaderHeight) + .padding(start = HeaderInsetStart, end = HeaderInsetEnd), verticalAlignment = Alignment.CenterVertically ) { // MARK: - Identity cluster. @@ -615,7 +718,7 @@ private fun MainHeader( contentDescription = stringResource(R.string.cd_unread_private_messages) ) { Icon( - imageVector = Icons.Filled.Email, + painter = painterResource(R.drawable.ic_spec_envelope), contentDescription = stringResource(R.string.cd_unread_private_messages), modifier = Modifier.size(HeaderIconSize), tint = palette.accentOrange diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 85ea78bf..e9c8e343 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -406,7 +406,6 @@ fun ChatScreen(viewModel: ChatViewModel) { // Floating header - positioned absolutely at top, ignores keyboard ChatFloatingHeader( - headerHeight = headerHeight, selectedPrivatePeer = null, currentChannel = currentChannel, nickname = nickname, @@ -674,6 +673,7 @@ fun ChatInputSection( currentChannel = currentChannel, nickname = nickname, showMediaButtons = showMediaButtons, + mentionPeerIdentities = mentionPeerIdentities, modifier = Modifier.fillMaxWidth() ) } @@ -697,7 +697,6 @@ private const val BarBackgroundAlpha = 0.88f private const val HeaderOpaqueStop = 0.72f @Composable private fun ChatFloatingHeader( - headerHeight: Dp, selectedPrivatePeer: String?, currentChannel: String?, nickname: String, @@ -729,38 +728,31 @@ private fun ChatFloatingHeader( ) .windowInsetsPadding(WindowInsets.statusBars) // Extend into status bar area ) { - // A plain Row rather than M3's TopAppBar. TopAppBar silently injects a 4.dp horizontal - // pad plus a 12.dp title inset and applies its own minimum heights, which made the - // header's spacing impossible to specify exactly. - Row( - modifier = Modifier - .fillMaxWidth() - .height(headerHeight) - .padding(start = 12.dp, end = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - ChatHeaderContent( - selectedPrivatePeer = selectedPrivatePeer, - currentChannel = currentChannel, - nickname = nickname, - viewModel = viewModel, - onBackClick = { - when { - selectedPrivatePeer != null -> viewModel.endPrivateChat() - currentChannel != null -> viewModel.switchToChannel(null) - } - }, - onSidebarClick = onSidebarToggle, - onTripleClick = onPanicClear, - onShowAppInfo = onShowAppInfo, - onLocationChannelsClick = onLocationChannelsClick, - onLocationNotesClick = { - // Ensure location is loaded before showing sheet - locationManager.refreshChannels() - onLocationNotesClick() + // No TopAppBar: it silently injects a 4.dp horizontal pad plus a 12.dp title inset and + // applies its own minimum heights, which made the header's spacing impossible to specify + // exactly. Height and edge insets belong to each header variant, so that a conversation + // header rendered here and one rendered in a sheet are laid out identically. + ChatHeaderContent( + selectedPrivatePeer = selectedPrivatePeer, + currentChannel = currentChannel, + nickname = nickname, + viewModel = viewModel, + onBackClick = { + when { + selectedPrivatePeer != null -> viewModel.endPrivateChat() + currentChannel != null -> viewModel.switchToChannel(null) } - ) - } + }, + onSidebarClick = onSidebarToggle, + onTripleClick = onPanicClear, + onShowAppInfo = onShowAppInfo, + onLocationChannelsClick = onLocationChannelsClick, + onLocationNotesClick = { + // Ensure location is loaded before showing sheet + locationManager.refreshChannels() + onLocationNotesClick() + } + ) } } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt index 19d52706..d2ab06d3 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt @@ -31,6 +31,15 @@ internal const val MENTION_CHIP_ALPHA = ChatVisualTokens.HighlightAlpha /** Background opacity for a mention chip referring to you. Slightly stronger to catch the eye. */ internal const val MENTION_CHIP_ALPHA_SELF = ChatVisualTokens.HighlightAlpha +/** + * Mention token grammar shared by rendered messages and the composer. + * + * The optional `#abcd` suffix is part of the mention because it disambiguates peers that use the + * same nickname. Keeping one regex prevents the composer from styling only `@name` while the + * rendered transcript styles the complete token. + */ +internal val MENTION_TOKEN_REGEX = Regex("@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)") + /** * Get RSSI-based color for signal strength visualization */ @@ -376,6 +385,23 @@ internal fun resolveMentionPeerIdentity( ?: mentionPeerIdentities[baseName.lowercase(Locale.ROOT)] } +/** + * Resolve the deterministic color for a mention on every surface that displays one. + * + * Exact suffixed tokens win; an unsuffixed nickname is only present in the identity map when it is + * unambiguous. The nickname fallback preserves the legacy behavior for peers with no stable ID. + */ +internal fun colorForMention( + mention: String, + mentionPeerIdentities: Map, + palette: BitchatPalette, +): Color { + val mentionWithoutAt = mention.trim().removePrefix("@") + val identity = resolveMentionPeerIdentity(mentionWithoutAt, mentionPeerIdentities) + ?: PeerIdentity.nickname(mentionWithoutAt) + return colorForPeer(identity, palette) +} + /** * A bare `anon` label means the geohash heartbeat has not announced a username yet. The transport * may append a `#abcd` disambiguator, which does not turn it into an announced name. Names such as @@ -408,12 +434,10 @@ private fun appendIOSFormattedContent( linkColor: Color, mentionPeerIdentities: Map, ) { - // iOS-style patterns: allow optional '#abcd' suffix in mentions val hashtagPattern = "#([a-zA-Z0-9_]+)".toRegex() - val mentionPattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)".toRegex() val hashtagMatches = hashtagPattern.findAll(content).toList() - val mentionMatches = mentionPattern.findAll(content).toList() + val mentionMatches = MENTION_TOKEN_REGEX.findAll(content).toList() // Combine and sort matches, but exclude hashtags that overlap with mentions val mentionRanges = mentionMatches.map { it.range } @@ -513,14 +537,10 @@ private fun appendIOSFormattedContent( val mentionColor = if (isMentionToMe) { palette.accentOrange } else { - val identity = resolveMentionPeerIdentity( - mentionWithoutAt, - mentionPeerIdentities, - ) - ?: PeerIdentity.nickname(mentionWithoutAt) - colorForPeer( - identity, - palette + colorForMention( + mention = mentionWithoutAt, + mentionPeerIdentities = mentionPeerIdentities, + palette = palette, ) } val chipAlpha = if (isMentionToMe) MENTION_CHIP_ALPHA_SELF else MENTION_CHIP_ALPHA diff --git a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt index c946eaf6..7302379b 100644 --- a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue @@ -65,11 +64,10 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.withStyle import com.bitchat.android.ui.theme.BASE_FONT_SIZE +import com.bitchat.android.ui.theme.BitchatPalette import com.bitchat.android.ui.theme.BitchatMotion import com.bitchat.android.ui.theme.LocalBitchatPalette -import com.bitchat.android.ui.theme.colorForPeer import com.bitchat.android.features.voice.normalizeAmplitudeSample import com.bitchat.android.features.voice.AudioWaveformExtractor import com.bitchat.android.ui.media.RealtimeScrollingWaveform @@ -91,38 +89,22 @@ class SlashCommandVisualTransformation( ) : VisualTransformation { override fun filter(text: AnnotatedString): TransformedText { val slashCommandRegex = Regex("(/\\w+)(?=\\s|$)") - val annotatedString = buildAnnotatedString { - var lastIndex = 0 - - slashCommandRegex.findAll(text.text).forEach { match -> - // Add text before the match - if (match.range.first > lastIndex) { - append(text.text.substring(lastIndex, match.range.first)) - } - - // Add the styled slash command - withStyle( - style = SpanStyle( - color = commandColor, - fontFamily = BitchatFontFamily, - fontWeight = FontWeight.Medium, - background = commandBackground - ) - ) { - append(match.value) - } - - lastIndex = match.range.last + 1 - } - - // Add remaining text - if (lastIndex < text.text.length) { - append(text.text.substring(lastIndex)) - } + val builder = AnnotatedString.Builder(text) + slashCommandRegex.findAll(text.text).forEach { match -> + builder.addStyle( + style = SpanStyle( + color = commandColor, + fontFamily = BitchatFontFamily, + fontWeight = FontWeight.Medium, + background = commandBackground + ), + start = match.range.first, + end = match.range.last + 1, + ) } return TransformedText( - text = annotatedString, + text = builder.toAnnotatedString(), offsetMapping = OffsetMapping.Identity ) } @@ -133,45 +115,55 @@ class SlashCommandVisualTransformation( * while preserving cursor positioning and click handling */ class MentionVisualTransformation( - private val mentionColor: Color, - private val mentionBackground: Color, + private val mentionPeerIdentities: Map, + private val palette: BitchatPalette, ) : VisualTransformation { override fun filter(text: AnnotatedString): TransformedText { - val mentionRegex = Regex("@([a-zA-Z0-9_]+)") - val annotatedString = buildAnnotatedString { - var lastIndex = 0 - - mentionRegex.findAll(text.text).forEach { match -> - // Add text before the match - if (match.range.first > lastIndex) { - append(text.text.substring(lastIndex, match.range.first)) - } - - // Add the styled mention - withStyle( + val builder = AnnotatedString.Builder(text) + + MENTION_TOKEN_REGEX.findAll(text.text).forEach { match -> + val start = match.range.first + val end = match.range.last + 1 + val suffixOffset = match.value.lastIndexOf('#').takeIf { it > 0 } + val suffixStart = suffixOffset?.let(start::plus) ?: end + val mentionColor = colorForMention( + mention = match.value, + mentionPeerIdentities = mentionPeerIdentities, + palette = palette, + ) + + // Keep the whole token on one continuous color-derived chip. + builder.addStyle( + style = SpanStyle( + background = mentionColor.copy(alpha = MENTION_CHIP_ALPHA), + ), + start = start, + end = end, + ) + builder.addStyle( + style = SpanStyle( + color = mentionColor, + fontFamily = BitchatFontFamily, + fontWeight = FontWeight.SemiBold, + ), + start = start, + end = suffixStart, + ) + if (suffixStart < end) { + builder.addStyle( style = SpanStyle( - color = mentionColor, + color = mentionColor.copy(alpha = SUFFIX_ALPHA), fontFamily = BitchatFontFamily, fontWeight = FontWeight.SemiBold, - // Mirrors the mention chip used in rendered messages, so what you type - // looks like what everyone will see. - background = mentionBackground - ) - ) { - append(match.value) - } - - lastIndex = match.range.last + 1 - } - - // Add remaining text - if (lastIndex < text.text.length) { - append(text.text.substring(lastIndex)) + ), + start = suffixStart, + end = end, + ) } } return TransformedText( - text = annotatedString, + text = builder.toAnnotatedString(), offsetMapping = OffsetMapping.Identity ) } @@ -313,6 +305,7 @@ fun MessageInput( currentChannel: String?, nickname: String, showMediaButtons: Boolean, + mentionPeerIdentities: Map = emptyMap(), modifier: Modifier = Modifier ) { val palette = LocalBitchatPalette.current @@ -386,7 +379,11 @@ fun MessageInput( }), // Cap the growth so a pasted wall of text cannot swallow the message list. maxLines = 6, - visualTransformation = remember(palette) { + visualTransformation = remember( + palette, + colorScheme.primary, + mentionPeerIdentities, + ) { CombinedVisualTransformation( listOf( SlashCommandVisualTransformation( @@ -394,8 +391,8 @@ fun MessageInput( commandBackground = colorScheme.primary.copy(alpha = 0.14f), ), MentionVisualTransformation( - mentionColor = palette.accentOrange, - mentionBackground = palette.accentOrange.copy(alpha = MENTION_CHIP_ALPHA), + mentionPeerIdentities = mentionPeerIdentities, + palette = palette, ), ) ) @@ -715,6 +712,7 @@ fun MentionSuggestionsBox( modifier: Modifier = Modifier ) { val colorScheme = MaterialTheme.colorScheme + val palette = LocalBitchatPalette.current LazyColumn( modifier = modifier @@ -734,11 +732,13 @@ fun MentionSuggestionsBox( items = suggestions, key = { suggestion -> suggestion.lowercase() } ) { suggestion -> - val identity = resolveMentionPeerIdentity(suggestion, mentionPeerIdentities) - ?: PeerIdentity.nickname(suggestion) MentionSuggestionItem( suggestion = suggestion, - identity = identity, + userColor = colorForMention( + mention = suggestion, + mentionPeerIdentities = mentionPeerIdentities, + palette = palette, + ), onClick = { onSuggestionClick(suggestion) }, modifier = Modifier.animateItem( fadeInSpec = tween( @@ -759,12 +759,11 @@ fun MentionSuggestionsBox( @Composable fun MentionSuggestionItem( suggestion: String, - identity: PeerIdentity, + userColor: Color, onClick: () -> Unit, modifier: Modifier = Modifier ) { val palette = LocalBitchatPalette.current - val userColor = colorForPeer(identity, palette) val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val pressedBackground by animateColorAsState( diff --git a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt index 5f2394bc..3d04d05e 100644 --- a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt @@ -38,6 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bitchat.android.core.ui.component.button.CloseButton import com.bitchat.android.core.ui.component.sheet.BitchatBottomSheet import com.bitchat.android.core.ui.component.sheet.BitchatSheetCenterTopBar +import com.bitchat.android.core.ui.component.sheet.LocalSheetDismiss import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar import com.bitchat.android.favorites.FavoriteRelationship @@ -614,7 +615,7 @@ private fun PeerItem( ) } else if (hasUnreadDM) { Icon( - imageVector = Icons.Filled.Email, + painter = painterResource(R.drawable.ic_spec_envelope), contentDescription = stringResource(R.string.cd_unread_message), modifier = Modifier.size(PeerRowIconSize), tint = palette.accentOrange @@ -635,11 +636,13 @@ private fun PeerItem( ) } else { Icon( - imageVector = when { - isWifiAware -> Icons.Filled.Wifi - isDirect -> Icons.Outlined.Bluetooth - else -> Icons.Filled.Route - }, + painter = painterResource( + conversationTransportIcon( + isReachedOverInternet = false, + isWifiAware = isWifiAware, + isDirect = isDirect + ) + ), contentDescription = when { isWifiAware -> "Direct Wi-Fi Aware" isDirect -> "Direct Bluetooth" @@ -680,7 +683,7 @@ private fun PeerItem( if (isVerified) { Icon( - imageVector = Icons.Filled.Verified, + painter = painterResource(R.drawable.ic_spec_check), contentDescription = stringResource(R.string.verify_title), modifier = Modifier.size(16.dp), tint = colorScheme.primary @@ -861,12 +864,7 @@ fun PrivateChatSheet( viewModel.isPeerVerified(peerID, verifiedFingerprints) } - val securityModifier = if (!isNostrPeer && !isNostrReachableFavorite) { - Modifier.clickable { viewModel.showSecurityVerificationSheet() } - } else { - Modifier - } - + val palette = LocalBitchatPalette.current val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ) @@ -880,7 +878,7 @@ fun PrivateChatSheet( Column( modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.height(64.dp)) + Spacer(modifier = Modifier.height(ChatHeaderHeight)) HorizontalDivider(thickness = 1.dp, color = colorScheme.outlineVariant) @@ -948,118 +946,99 @@ fun PrivateChatSheet( ) } - // TopBar (fixed at top, iOS-style) - BitchatSheetCenterTopBar( - onClose = onDismiss, + // Header. Built from the same tokens as the main chat header rather than a + // TopAppBar, so moving between the timeline and a conversation does not shift the + // bar's height, insets or type. + Surface( modifier = Modifier.align(Alignment.TopCenter), - navigationIcon = { - IconButton( - onClick = onDismiss, - modifier = Modifier - .align(Alignment.CenterStart) - .padding(start = 16.dp) - .size(32.dp) + color = colorScheme.background + ) { + ConversationHeader( + leadingIconRes = conversationTransportIcon( + isReachedOverInternet = isNostrPeer || isNostrReachableFavorite, + isWifiAware = isWifiAware, + isDirect = isDirect + ), + // Reachability is a status, not an alert: it takes the muted chrome tint, + // leaving the primary green for the name itself. + leadingIconTint = colorScheme.onSurfaceVariant, + leadingContentDescription = when { + isNostrPeer || isNostrReachableFavorite -> + stringResource(R.string.cd_nostr_reachable) + else -> null + }, + title = titleText + ) { + ConversationHeaderAction( + onClick = { viewModel.toggleFavorite(peerID) }, + contentDescription = if (isFavorite) { + stringResource(R.string.cd_remove_favorite) + } else { + stringResource(R.string.cd_add_favorite) + } ) { Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.chat_back), - tint = colorScheme.onSurface + painter = painterResource( + if (isFavorite) { + R.drawable.ic_spec_star_filled + } else { + R.drawable.ic_spec_star + } + ), + contentDescription = null, + modifier = Modifier.size(HeaderIconSize), + tint = if (isFavorite) { + palette.accentOrange + } else { + colorScheme.onSurfaceVariant + } ) } - }, - title = { - // Center content: connection status + name + encryption - Row( - modifier = Modifier.align(Alignment.Center), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - when { - isNostrPeer || isNostrReachableFavorite -> { - Icon( - painter = painterResource(R.drawable.ic_spec_globe), - contentDescription = stringResource(R.string.cd_nostr_reachable), - modifier = Modifier.size(14.dp), - tint = Color(0xFF9C27B0) - ) - } - isWifiAware -> { - Icon( - imageVector = Icons.Filled.Wifi, - contentDescription = "Direct Wi-Fi Aware", - modifier = Modifier.size(14.dp), - tint = colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - isDirect -> { - Icon( - imageVector = Icons.Outlined.Bluetooth, - contentDescription = "Direct Bluetooth", - modifier = Modifier.size(14.dp), - tint = colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - isConnected -> { - Icon( - imageVector = Icons.Filled.Route, - contentDescription = "Routed", - modifier = Modifier.size(14.dp), - tint = colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - } - Text( - text = titleText, - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Bold, - fontFamily = BitchatFontFamily - ), - color = if (isNostrPeer || isNostrReachableFavorite) Color(0xFFFF9500) else colorScheme.onSurface - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.then(securityModifier) + // Encryption state, and the verification badge that qualifies it. Both are + // read-only for Nostr peers, which have no Noise session at all. + if (!isNostrPeer && !isNostrReachableFavorite) { + ConversationHeaderAction( + onClick = { viewModel.showSecurityVerificationSheet() }, + contentDescription = stringResource(R.string.verify_title) ) { - if (!isNostrPeer && !isNostrReachableFavorite) { + Box(contentAlignment = Alignment.Center) { NoiseSessionIcon( sessionState = sessionState, - modifier = Modifier.size(14.dp) - ) - } - - if (isVerified) { - Spacer(modifier = Modifier.width(4.dp)) - Icon( - imageVector = Icons.Filled.Verified, - contentDescription = stringResource(R.string.verify_title), - modifier = Modifier.size(14.dp), - tint = Color(0xFF32D74B) // iOS Green + modifier = Modifier.size(HeaderIconSize) ) } } + } - IconButton( - onClick = { viewModel.toggleFavorite(peerID) }, - modifier = Modifier.size(28.dp) + if (isVerified) { + Box( + modifier = Modifier.size(HeaderIconSize), + contentAlignment = Alignment.Center ) { Icon( - painter = painterResource( - if (isFavorite) { - R.drawable.ic_spec_star_filled - } else { - R.drawable.ic_spec_star - } - ), - contentDescription = if (isFavorite) stringResource(R.string.cd_remove_favorite) else stringResource(R.string.cd_add_favorite), - modifier = Modifier.size(16.dp), - tint = if (isFavorite) Color(0xFFFFD700) else colorScheme.onSurface.copy(alpha = 0.6f) + painter = painterResource(R.drawable.ic_spec_check), + contentDescription = stringResource(R.string.verify_title), + modifier = Modifier.size(HeaderIconSize), + tint = colorScheme.primary ) } } + + val dismiss = LocalSheetDismiss.current + ConversationHeaderAction( + onClick = { dismiss?.invoke() ?: onDismiss() }, + contentDescription = stringResource(R.string.close_plain) + ) { + Icon( + painter = painterResource(R.drawable.ic_spec_close), + contentDescription = null, + modifier = Modifier.size(HeaderIconSize), + tint = colorScheme.onSurfaceVariant + ) + } } - ) + } } } } diff --git a/app/src/main/res/drawable/ic_spec_bluetooth.xml b/app/src/main/res/drawable/ic_spec_bluetooth.xml new file mode 100644 index 00000000..8c1ed7ae --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_bluetooth.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_spec_check.xml b/app/src/main/res/drawable/ic_spec_check.xml new file mode 100644 index 00000000..16eb9e1c --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_check.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_spec_close.xml b/app/src/main/res/drawable/ic_spec_close.xml new file mode 100644 index 00000000..e0fbc4ea --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_close.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_spec_envelope.xml b/app/src/main/res/drawable/ic_spec_envelope.xml new file mode 100644 index 00000000..15552d37 --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_envelope.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_spec_lock_open.xml b/app/src/main/res/drawable/ic_spec_lock_open.xml new file mode 100644 index 00000000..11d69821 --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_lock_open.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_spec_routed.xml b/app/src/main/res/drawable/ic_spec_routed.xml new file mode 100644 index 00000000..36e271d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_routed.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_spec_sync.xml b/app/src/main/res/drawable/ic_spec_sync.xml new file mode 100644 index 00000000..1ab434e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_sync.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_spec_warning.xml b/app/src/main/res/drawable/ic_spec_warning.xml new file mode 100644 index 00000000..ffb64e3c --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_warning.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_spec_wifi.xml b/app/src/main/res/drawable/ic_spec_wifi.xml new file mode 100644 index 00000000..4757653f --- /dev/null +++ b/app/src/main/res/drawable/ic_spec_wifi.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index f78c1a95..13c8d51c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -71,8 +71,6 @@ إضافة إشارة مرجعية - رجوع - مغادرة يمكن الوصول عبر Nostr رسائل خاصة غير مقروءة تم الانتقال diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 30abcfbd..7c2617a4 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -71,8 +71,6 @@ বুকমার্ক যোগ করুন - ফিরে যান - ছেড়ে দিন Nostr এর মাধ্যমে পৌঁছানো যায় অপঠিত ব্যক্তিগত বার্তা টেলিপোর্ট করা হয়েছে diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d8338a41..b13fb001 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -71,8 +71,6 @@ Lesezeichen hinzufügen - zurück - verlassen Über Nostr erreichbar Ungelesene private Nachrichten Teleportiert diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index eefb901b..e1ac2c2b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -71,8 +71,6 @@ Agregar marcador - atrás - salir Accesible vía Nostr Mensajes privados sin leer Teletransportado diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 8f1d0bf0..a3e81d29 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -71,8 +71,6 @@ افزودن نشانک - بازگشت - خروج قابل دسترس از طریق Nostr پیام‌های خصوصی خوانده‌نشده انتقال مکانی diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 89724eb6..1da03f06 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -71,8 +71,6 @@ Mag‑bookmark - bumalik - umalis Maabot sa Nostr Hindi nabasang pribadong mensahe Naiteleport diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 66f653ce..e632bdb8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -71,8 +71,6 @@ Ajouter un signet - retour - quitter Joignable via Nostr Messages privés non lus Téléporté diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 87948601..1a393f38 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -71,8 +71,6 @@ बुकमार्क जोड़ें - वापस - छोड़ें Nostr के माध्यम से उपलब्ध अपठित निजी संदेश टेलीपोर्ट हुआ diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 1a11487c..934556e4 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -71,8 +71,6 @@ Tambah bookmark - kembali - keluar Dapat dijangkau melalui Nostr Pesan pribadi yang belum dibaca Diteleportasi diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 36feb8cf..7e67de6d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -71,8 +71,6 @@ Aggiungi segnalibro - indietro - esci Raggiungibile via Nostr Messaggi privati non letti Teletrasportato diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 9b2844c2..1e7da71d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -71,8 +71,6 @@ ブックマークに追加 - 戻る - 退出 Nostr 経由で到達可能 未読のプライベートメッセージ テレポート済み diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index d1d19dad..4994f9cc 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -71,8 +71,6 @@ სანიშნეს დამატება - უკან - გასვლა მისაწვდომია Nostr-ით წაუკითხავი პირადი შეტყობინებები ტელეპორტირებული diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d72df76f..7327257a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -71,8 +71,6 @@ 북마크 추가 - 뒤로 - 나가기 Nostr로 연결 가능 읽지 않은 개인 메시지 텔레포트됨 diff --git a/app/src/main/res/values-mg/strings.xml b/app/src/main/res/values-mg/strings.xml index a15540b1..168eebe7 100644 --- a/app/src/main/res/values-mg/strings.xml +++ b/app/src/main/res/values-mg/strings.xml @@ -71,8 +71,6 @@ Hanampy bookmark - miverina - hiala Azo tratrarina amin\'ny alalan\'ny Nostr Hafatra manokana tsy voavaky Nafindra toerana diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index b9df0f99..48a7afbd 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -71,8 +71,6 @@ बुकमार्क थप्नुहोस् - पछाडि - छोड्नुहोस् Nostr मार्फत पुग्न सकिने नपढिएका निजी सन्देश टेलिपोर्ट भयो diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 8328b8ff..0eeaec55 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -71,8 +71,6 @@ Bladwijzer toevoegen - terug - verlaten Bereikbaar via Nostr Ongelezen privéberichten Geteleporteerd diff --git a/app/src/main/res/values-pa-rPK/strings.xml b/app/src/main/res/values-pa-rPK/strings.xml index 4079133a..5f5f5f75 100644 --- a/app/src/main/res/values-pa-rPK/strings.xml +++ b/app/src/main/res/values-pa-rPK/strings.xml @@ -71,8 +71,6 @@ بُک مارک پاؤ - واپس - باہر آؤ Nostr راہین پہنچ ممکن اے انہ پڑھیا ذاتی پیغام ٹیلپورٹ کیتا گیا diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2bf02cb5..fd4901ef 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -71,8 +71,6 @@ Adicionar favorito - voltar - sair Acessível via Nostr Mensagens privadas não lidas Teletransportado diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index e45c26a6..5247916e 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -71,8 +71,6 @@ Adicionar marcador - voltar - sair Alcançável via Nostr Mensagens privadas não lidas Teletransportado diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1230f4be..4dc08928 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -63,8 +63,6 @@ Убрать из избранного Добавить закладку - назад - выйти Доступен через Nostr Непрочитанные личные Телепортировано diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 17cc2963..dcfda208 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -63,8 +63,6 @@ Ta bort favorit Lägg till bokmärke - tillbaka - lämna Nås via Nostr Olästa privata meddelanden Teleporterad diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 53dc6610..7e1fc05a 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -71,8 +71,6 @@ เพิ่มที่คั่นหน้า - กลับ - ออก ติดต่อได้ผ่าน Nostr ข้อความส่วนตัวยังไม่ได้อ่าน เทเลพอร์ตแล้ว diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e7eb1b09..b81fdd08 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -63,8 +63,6 @@ Sık kullanılandan kaldır Yer imi ekle - geri - ayrıl Nostr üzerinden ulaşılabilir Okunmamış özel mesaj Teleport edildi diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 6bc67120..36c5265d 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -71,8 +71,6 @@ بک مارک شامل کریں - واپس - چھوڑیں Nostr کے ذریعے رسائی غیر پڑھے ہوئے نجی پیغامات ٹیلی پورٹ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 7e1c9ef3..20028f38 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -71,8 +71,6 @@ Thêm bookmark - quay lại - rời khỏi Có thể tiếp cận qua Nostr Tin nhắn riêng chưa đọc Đã dịch chuyển diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index a5758b89..d6eada8c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -71,8 +71,6 @@ 添加书签 - 返回 - 离开 可通过 Nostr 联系 未读私信 已传送 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5ee5c4c1..02393f74 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,8 +72,6 @@ Add bookmark - Back - Leave Nostr reachable Unread private messages Location notes diff --git a/app/src/test/java/com/bitchat/android/ui/ChatUIUtilsTest.kt b/app/src/test/java/com/bitchat/android/ui/ChatUIUtilsTest.kt index 84142117..e64448c3 100644 --- a/app/src/test/java/com/bitchat/android/ui/ChatUIUtilsTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/ChatUIUtilsTest.kt @@ -1,6 +1,7 @@ package com.bitchat.android.ui import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp @@ -252,6 +253,40 @@ class ChatUIUtilsTest { assertTrue(body.spanStyles.any { it.item.color == expected }) } + @Test + fun `composer colors nickname and hash suffix from the mentioned peer identity`() { + val pubkey = "0123456789abcdef".repeat(4) + val identity = PeerIdentity.nostr(pubkey) + val token = "@carol#04af" + val input = "ping $token now" + val transformed = MentionVisualTransformation( + mentionPeerIdentities = mapOf("carol#04af" to identity), + palette = palette, + ).filter(AnnotatedString(input)).text + + val expectedColor = colorForPeer(identity, palette) + val tokenStart = input.indexOf(token) + val suffixStart = input.indexOf("#04af") + val tokenEnd = tokenStart + token.length + + assertEquals(input, transformed.text) + assertTrue(transformed.spanStyles.any { + it.start == tokenStart && + it.end == tokenEnd && + it.item.background == expectedColor.copy(alpha = MENTION_CHIP_ALPHA) + }) + assertTrue(transformed.spanStyles.any { + it.start == tokenStart && + it.end == suffixStart && + it.item.color == expectedColor + }) + assertTrue(transformed.spanStyles.any { + it.start == suffixStart && + it.end == tokenEnd && + it.item.color == expectedColor.copy(alpha = SUFFIX_ALPHA) + }) + } + @Test fun `ambiguous base nickname is not assigned to the wrong peer`() { val firstIdentity = PeerIdentity.nostr("11111111".repeat(8))