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.
This commit is contained in:
callebtc 2026-07-27 16:54:58 +02:00
parent 5c8f03ce18
commit e66ddde063
42 changed files with 482 additions and 324 deletions

View File

@ -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

View File

@ -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()
}
)
}
}

View File

@ -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<String, PeerIdentity>,
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<String, PeerIdentity>,
) {
// 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

View File

@ -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<String, PeerIdentity>,
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<String, PeerIdentity> = 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(

View File

@ -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
)
}
}
)
}
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M10,2.75V17.25"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M10,2.75L14.5,7.25L5.5,12.75"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M10,17.25L14.5,12.75L5.5,7.25"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M4.25,10.5L8.25,14.5L15.75,6"/>
</vector>

View File

@ -0,0 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M5.5,5.5L14.5,14.5"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M14.5,5.5L5.5,14.5"/>
</vector>

View File

@ -0,0 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M15.75,15.5H4.25C3.2145,15.5 2.375,14.6605 2.375,13.625V6.375C2.375,5.3395 3.2145,4.5 4.25,4.5H15.75C16.7855,4.5 17.625,5.3395 17.625,6.375V13.625C17.625,14.6605 16.7855,15.5 15.75,15.5Z"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M3.1,5.75L10,10.85L16.9,5.75"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<!-- Body and shackle geometry copied from ic_spec_lock so the two states sit on exactly the
same baseline; only the shackle is swung open, hinged on its right leg. -->
<group android:translateX="-16" android:translateY="-310">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M32.25,329.375H19.75C18.715,329.375 17.875,328.535 17.875,327.5V321.25C17.875,320.215 18.715,319.375 19.75,319.375H32.25C33.285,319.375 34.125,320.215 34.125,321.25V327.5C34.125,328.535 33.285,329.375 32.25,329.375Z"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M34.125,316.875V315C34.125,312.584 32.1662,310.625 29.75,310.625C27.3338,310.625 25.375,312.584 25.375,315V316.5"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M26,326.25C27.0355,326.25 27.875,325.411 27.875,324.375C27.875,323.339 27.0355,322.5 26,322.5C24.9645,322.5 24.125,323.339 24.125,324.375C24.125,325.411 24.9645,326.25 26,326.25Z"/>
</group>
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M6,5.25A1.75,1.75 0 1,1 2.5,5.25A1.75,1.75 0 1,1 6,5.25Z"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M17.5,5.25A1.75,1.75 0 1,1 14,5.25A1.75,1.75 0 1,1 17.5,5.25Z"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M11.75,15.75A1.75,1.75 0 1,1 8.25,15.75A1.75,1.75 0 1,1 11.75,15.75Z"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M5.1,6.9L9.3,14.1"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M14.9,6.9L10.7,14.1"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M3.75,10A6.25,6.25 0 0,1 14.9,6.15"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M11.4,6.6L15.1,6.4L14.9,2.7"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M16.25,10A6.25,6.25 0 0,1 5.1,13.85"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M8.6,13.4L4.9,13.6L5.1,17.3"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M10,3.25L17.75,16.75H2.25L10,3.25Z"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M10,8V11.5"/>
<path android:fillColor="#FFFFFFFF" android:pathData="M10.9,14.25A0.9,0.9 0 1,1 9.1,14.25A0.9,0.9 0 1,1 10.9,14.25Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M3.5,8.5C6.1,5.7 13.9,5.7 16.5,8.5"/>
<path android:fillColor="#00000000" android:strokeColor="#FFFFFFFF" android:strokeWidth="1.25" android:strokeLineCap="round" android:strokeLineJoin="round" android:pathData="M6.4,11.6C8.1,9.8 11.9,9.8 13.6,11.6"/>
<path android:fillColor="#FFFFFFFF" android:pathData="M11,15.25A1,1 0 1,1 9,15.25A1,1 0 1,1 11,15.25Z"/>
</vector>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">إضافة إشارة مرجعية</string>
<!-- ترويسة الدردشة وسهولة الوصول -->
<string name="chat_back">رجوع</string>
<string name="chat_leave">مغادرة</string>
<string name="cd_nostr_reachable">يمكن الوصول عبر Nostr</string>
<string name="cd_unread_private_messages">رسائل خاصة غير مقروءة</string>
<string name="cd_teleported">تم الانتقال</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">বুকমার্ক যোগ করুন</string>
<!-- চ্যাট হেডার ও অ্যাক্সেসিবিলিটি -->
<string name="chat_back">ফিরে যান</string>
<string name="chat_leave">ছেড়ে দিন</string>
<string name="cd_nostr_reachable">Nostr এর মাধ্যমে পৌঁছানো যায়</string>
<string name="cd_unread_private_messages">অপঠিত ব্যক্তিগত বার্তা</string>
<string name="cd_teleported">টেলিপোর্ট করা হয়েছে</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Lesezeichen hinzufügen</string>
<!-- ChatKopf &amp; Barrierefreiheit -->
<string name="chat_back">zurück</string>
<string name="chat_leave">verlassen</string>
<string name="cd_nostr_reachable">Über Nostr erreichbar</string>
<string name="cd_unread_private_messages">Ungelesene private Nachrichten</string>
<string name="cd_teleported">Teleportiert</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Agregar marcador</string>
<!-- Encabezado de chat y accesibilidad -->
<string name="chat_back">atrás</string>
<string name="chat_leave">salir</string>
<string name="cd_nostr_reachable">Accesible vía Nostr</string>
<string name="cd_unread_private_messages">Mensajes privados sin leer</string>
<string name="cd_teleported">Teletransportado</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">افزودن نشانک</string>
<!-- سربرگ چت و دسترسی‌پذیری -->
<string name="chat_back">بازگشت</string>
<string name="chat_leave">خروج</string>
<string name="cd_nostr_reachable">قابل دسترس از طریق Nostr</string>
<string name="cd_unread_private_messages">پیام‌های خصوصی خوانده‌نشده</string>
<string name="cd_teleported">انتقال مکانی</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Magbookmark</string>
<!-- Header at accessibility -->
<string name="chat_back">bumalik</string>
<string name="chat_leave">umalis</string>
<string name="cd_nostr_reachable">Maabot sa Nostr</string>
<string name="cd_unread_private_messages">Hindi nabasang pribadong mensahe</string>
<string name="cd_teleported">Naiteleport</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Ajouter un signet</string>
<!-- Entête de chat &amp; accessibilité -->
<string name="chat_back">retour</string>
<string name="chat_leave">quitter</string>
<string name="cd_nostr_reachable">Joignable via Nostr</string>
<string name="cd_unread_private_messages">Messages privés non lus</string>
<string name="cd_teleported">Téléporté</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">बुकमार्क जोड़ें</string>
<!-- चैट हेडर &amp; एक्सेसिबिलिटी -->
<string name="chat_back">वापस</string>
<string name="chat_leave">छोड़ें</string>
<string name="cd_nostr_reachable">Nostr के माध्यम से उपलब्ध</string>
<string name="cd_unread_private_messages">अपठित निजी संदेश</string>
<string name="cd_teleported">टेलीपोर्ट हुआ</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Tambah bookmark</string>
<!-- Header chat &amp; aksesibilitas -->
<string name="chat_back">kembali</string>
<string name="chat_leave">keluar</string>
<string name="cd_nostr_reachable">Dapat dijangkau melalui Nostr</string>
<string name="cd_unread_private_messages">Pesan pribadi yang belum dibaca</string>
<string name="cd_teleported">Diteleportasi</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Aggiungi segnalibro</string>
<!-- Intestazione chat e accessibilità -->
<string name="chat_back">indietro</string>
<string name="chat_leave">esci</string>
<string name="cd_nostr_reachable">Raggiungibile via Nostr</string>
<string name="cd_unread_private_messages">Messaggi privati non letti</string>
<string name="cd_teleported">Teletrasportato</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">ブックマークに追加</string>
<!-- チャットヘッダー &amp; アクセシビリティ -->
<string name="chat_back">戻る</string>
<string name="chat_leave">退出</string>
<string name="cd_nostr_reachable">Nostr 経由で到達可能</string>
<string name="cd_unread_private_messages">未読のプライベートメッセージ</string>
<string name="cd_teleported">テレポート済み</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">სანიშნეს დამატება</string>
<!-- ჩათის სათაური &amp; ხელმისაწვდომობა -->
<string name="chat_back">უკან</string>
<string name="chat_leave">გასვლა</string>
<string name="cd_nostr_reachable">მისაწვდომია Nostr-ით</string>
<string name="cd_unread_private_messages">წაუკითხავი პირადი შეტყობინებები</string>
<string name="cd_teleported">ტელეპორტირებული</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">북마크 추가</string>
<!-- 채팅 헤더 &amp; 접근성 -->
<string name="chat_back">뒤로</string>
<string name="chat_leave">나가기</string>
<string name="cd_nostr_reachable">Nostr로 연결 가능</string>
<string name="cd_unread_private_messages">읽지 않은 개인 메시지</string>
<string name="cd_teleported">텔레포트됨</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Hanampy bookmark</string>
<!-- Lohan\'ny resaka &amp; fahafaha-miditra -->
<string name="chat_back">miverina</string>
<string name="chat_leave">hiala</string>
<string name="cd_nostr_reachable">Azo tratrarina amin\'ny alalan\'ny Nostr</string>
<string name="cd_unread_private_messages">Hafatra manokana tsy voavaky</string>
<string name="cd_teleported">Nafindra toerana</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">बुकमार्क थप्नुहोस्</string>
<!-- च्याट शीर्षक / पहुँचयोग्यता -->
<string name="chat_back">पछाडि</string>
<string name="chat_leave">छोड्नुहोस्</string>
<string name="cd_nostr_reachable">Nostr मार्फत पुग्न सकिने</string>
<string name="cd_unread_private_messages">नपढिएका निजी सन्देश</string>
<string name="cd_teleported">टेलिपोर्ट भयो</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Bladwijzer toevoegen</string>
<!-- Chatkop &amp; toegankelijkheid -->
<string name="chat_back">terug</string>
<string name="chat_leave">verlaten</string>
<string name="cd_nostr_reachable">Bereikbaar via Nostr</string>
<string name="cd_unread_private_messages">Ongelezen privéberichten</string>
<string name="cd_teleported">Geteleporteerd</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">بُک مارک پاؤ</string>
<!-- چیٹ ہیڈر &amp; رسائی پذیری -->
<string name="chat_back">واپس</string>
<string name="chat_leave">باہر آؤ</string>
<string name="cd_nostr_reachable">Nostr راہین پہنچ ممکن اے</string>
<string name="cd_unread_private_messages">انہ پڑھیا ذاتی پیغام</string>
<string name="cd_teleported">ٹیلپورٹ کیتا گیا</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Adicionar favorito</string>
<!-- Cabeçalho do chat &amp; acessibilidade -->
<string name="chat_back">voltar</string>
<string name="chat_leave">sair</string>
<string name="cd_nostr_reachable">Acessível via Nostr</string>
<string name="cd_unread_private_messages">Mensagens privadas não lidas</string>
<string name="cd_teleported">Teletransportado</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Adicionar marcador</string>
<!-- Cabeçalho do chat e acessibilidade -->
<string name="chat_back">voltar</string>
<string name="chat_leave">sair</string>
<string name="cd_nostr_reachable">Alcançável via Nostr</string>
<string name="cd_unread_private_messages">Mensagens privadas não lidas</string>
<string name="cd_teleported">Teletransportado</string>

View File

@ -63,8 +63,6 @@
<string name="cd_remove_favorite">Убрать из избранного</string>
<string name="cd_add_bookmark">Добавить закладку</string>
<string name="chat_back">назад</string>
<string name="chat_leave">выйти</string>
<string name="cd_nostr_reachable">Доступен через Nostr</string>
<string name="cd_unread_private_messages">Непрочитанные личные</string>
<string name="cd_teleported">Телепортировано</string>

View File

@ -63,8 +63,6 @@
<string name="cd_remove_favorite">Ta bort favorit</string>
<string name="cd_add_bookmark">Lägg till bokmärke</string>
<string name="chat_back">tillbaka</string>
<string name="chat_leave">lämna</string>
<string name="cd_nostr_reachable">Nås via Nostr</string>
<string name="cd_unread_private_messages">Olästa privata meddelanden</string>
<string name="cd_teleported">Teleporterad</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">เพิ่มที่คั่นหน้า</string>
<!-- ส่วนหัวแชท &amp; การช่วยสำหรับการเข้าถึง -->
<string name="chat_back">กลับ</string>
<string name="chat_leave">ออก</string>
<string name="cd_nostr_reachable">ติดต่อได้ผ่าน Nostr</string>
<string name="cd_unread_private_messages">ข้อความส่วนตัวยังไม่ได้อ่าน</string>
<string name="cd_teleported">เทเลพอร์ตแล้ว</string>

View File

@ -63,8 +63,6 @@
<string name="cd_remove_favorite">Sık kullanılandan kaldır</string>
<string name="cd_add_bookmark">Yer imi ekle</string>
<string name="chat_back">geri</string>
<string name="chat_leave">ayrıl</string>
<string name="cd_nostr_reachable">Nostr üzerinden ulaşılabilir</string>
<string name="cd_unread_private_messages">Okunmamış özel mesaj</string>
<string name="cd_teleported">Teleport edildi</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">بک مارک شامل کریں</string>
<!-- چیٹ ہیڈر اور رسائی -->
<string name="chat_back">واپس</string>
<string name="chat_leave">چھوڑیں</string>
<string name="cd_nostr_reachable">Nostr کے ذریعے رسائی</string>
<string name="cd_unread_private_messages">غیر پڑھے ہوئے نجی پیغامات</string>
<string name="cd_teleported">ٹیلی پورٹ</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">Thêm bookmark</string>
<!-- Header trò chuyện &amp; khả năng truy cập -->
<string name="chat_back">quay lại</string>
<string name="chat_leave">rời khỏi</string>
<string name="cd_nostr_reachable">Có thể tiếp cận qua Nostr</string>
<string name="cd_unread_private_messages">Tin nhắn riêng chưa đọc</string>
<string name="cd_teleported">Đã dịch chuyển</string>

View File

@ -71,8 +71,6 @@
<string name="cd_add_bookmark">添加书签</string>
<!-- 聊天头部 / 可访问性 -->
<string name="chat_back">返回</string>
<string name="chat_leave">离开</string>
<string name="cd_nostr_reachable">可通过 Nostr 联系</string>
<string name="cd_unread_private_messages">未读私信</string>
<string name="cd_teleported">已传送</string>

View File

@ -72,8 +72,6 @@
<string name="cd_add_bookmark">Add bookmark</string>
<!-- Chat header &amp; accessibility -->
<string name="chat_back">Back</string>
<string name="chat_leave">Leave</string>
<string name="cd_nostr_reachable">Nostr reachable</string>
<string name="cd_unread_private_messages">Unread private messages</string>
<string name="cd_location_notes">Location notes</string>

View File

@ -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))