Translations (#467)

* english done

* de

* more extraction

* wip strings en

* translations work

* remove unneeded translations

* remove notification message

* add languages

* new languages
This commit is contained in:
callebtc
2025-10-12 18:54:20 +02:00
committed by GitHub
parent 3b2241e891
commit ad28cc710c
64 changed files with 10051 additions and 347 deletions
@@ -29,7 +29,8 @@ import androidx.compose.ui.unit.sp
import com.bitchat.android.nostr.NostrProofOfWork
import com.bitchat.android.nostr.PoWPreferenceManager
import com.bitchat.android.ui.debug.DebugSettingsSheet
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
/**
* About Sheet for bitchat app information
* Matches the design language of LocationChannelsSheet
@@ -102,7 +103,7 @@ fun AboutSheet(
verticalAlignment = Alignment.Bottom
) {
Text(
text = "bitchat",
text = stringResource(R.string.app_name),
style = TextStyle(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
@@ -112,7 +113,7 @@ fun AboutSheet(
)
Text(
text = "v$versionName",
text = stringResource(R.string.version_prefix, versionName?:""),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.5f),
@@ -123,7 +124,7 @@ fun AboutSheet(
}
Text(
text = "decentralized mesh messaging with end-to-end encryption",
text = stringResource(R.string.about_tagline),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
@@ -141,7 +142,7 @@ fun AboutSheet(
) {
Icon(
imageVector = Icons.Filled.Bluetooth,
contentDescription = "Offline Mesh Chat",
contentDescription = stringResource(R.string.cd_offline_mesh_chat),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
@@ -150,14 +151,14 @@ fun AboutSheet(
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = "Offline Mesh Chat",
text = stringResource(R.string.about_offline_mesh_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Communicate directly via Bluetooth LE without internet or servers. Messages relay through nearby devices to extend range.",
text = stringResource(R.string.about_offline_mesh_desc),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)
)
@@ -173,7 +174,7 @@ fun AboutSheet(
) {
Icon(
imageVector = Icons.Default.Public,
contentDescription = "Online Geohash Channels",
contentDescription = stringResource(R.string.cd_online_geohash_channels),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
@@ -182,14 +183,14 @@ fun AboutSheet(
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = "Online Geohash Channels",
text = stringResource(R.string.about_online_geohash_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Connect with people in your area using geohash-based channels. Extend the mesh using public internet relays.",
text = stringResource(R.string.about_online_geohash_desc),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)
)
@@ -205,7 +206,7 @@ fun AboutSheet(
) {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = "End-to-End Encryption",
contentDescription = stringResource(R.string.cd_end_to_end_encryption),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
@@ -214,14 +215,14 @@ fun AboutSheet(
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = "End-to-End Encryption",
text = stringResource(R.string.about_e2e_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Private messages are encrypted. Channel messages are public.",
text = stringResource(R.string.about_e2e_desc),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)
)
@@ -232,7 +233,7 @@ fun AboutSheet(
// Appearance Section
item(key = "appearance_section") {
Text(
text = "appearance",
text = stringResource(R.string.about_appearance),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier
@@ -247,24 +248,24 @@ fun AboutSheet(
FilterChip(
selected = themePref.isSystem,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.System) },
label = { Text("system", fontFamily = FontFamily.Monospace) }
label = { Text(stringResource(R.string.about_system), fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = themePref.isLight,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Light) },
label = { Text("light", fontFamily = FontFamily.Monospace) }
label = { Text(stringResource(R.string.about_light), fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = themePref.isDark,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Dark) },
label = { Text("dark", fontFamily = FontFamily.Monospace) }
label = { Text(stringResource(R.string.about_dark), fontFamily = FontFamily.Monospace) }
)
}
}
// Proof of Work Section
item(key = "pow_section") {
Text(
text = "proof of work",
text = stringResource(R.string.about_pow),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier
@@ -289,7 +290,7 @@ fun AboutSheet(
FilterChip(
selected = !powEnabled,
onClick = { PoWPreferenceManager.setPowEnabled(false) },
label = { Text("pow off", fontFamily = FontFamily.Monospace) }
label = { Text(stringResource(R.string.about_pow_off), fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = powEnabled,
@@ -299,7 +300,7 @@ fun AboutSheet(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("pow on", fontFamily = FontFamily.Monospace)
Text(stringResource(R.string.about_pow_on), fontFamily = FontFamily.Monospace)
// Show current difficulty
if (powEnabled) {
Surface(
@@ -313,7 +314,7 @@ fun AboutSheet(
}
Text(
text = "add proof of work to geohash messages for spam deterrence.",
text = stringResource(R.string.about_pow_tip),
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.6f)
@@ -326,7 +327,7 @@ fun AboutSheet(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "difficulty: $powDifficulty bits (~${NostrProofOfWork.estimateMiningTime(powDifficulty)})",
text = stringResource(R.string.about_pow_difficulty, powDifficulty, NostrProofOfWork.estimateMiningTime(powDifficulty)),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
)
@@ -353,20 +354,20 @@ fun AboutSheet(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "difficulty $powDifficulty requires ~${NostrProofOfWork.estimateWork(powDifficulty)} hash attempts",
text = stringResource(R.string.about_pow_difficulty_attempts, powDifficulty, NostrProofOfWork.estimateWork(powDifficulty)),
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f)
)
Text(
text = when {
powDifficulty == 0 -> "no proof of work required"
powDifficulty <= 8 -> "very low - minimal spam protection"
powDifficulty <= 12 -> "low - basic spam protection"
powDifficulty <= 16 -> "medium - good spam protection"
powDifficulty <= 20 -> "high - strong spam protection"
powDifficulty <= 24 -> "very high - may cause delays"
else -> "extreme - significant computation required"
powDifficulty == 0 -> stringResource(R.string.about_pow_desc_none)
powDifficulty <= 8 -> stringResource(R.string.about_pow_desc_very_low)
powDifficulty <= 12 -> stringResource(R.string.about_pow_desc_low)
powDifficulty <= 16 -> stringResource(R.string.about_pow_desc_medium)
powDifficulty <= 20 -> stringResource(R.string.about_pow_desc_high)
powDifficulty <= 24 -> stringResource(R.string.about_pow_desc_very_high)
else -> stringResource(R.string.about_pow_desc_extreme)
},
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
@@ -384,7 +385,7 @@ fun AboutSheet(
val torMode = remember { mutableStateOf(com.bitchat.android.net.TorPreferenceManager.get(context)) }
val torStatus by com.bitchat.android.net.TorManager.statusFlow.collectAsState()
Text(
text = "network",
text = stringResource(R.string.about_network),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier
@@ -429,7 +430,7 @@ fun AboutSheet(
)
}
Text(
text = "route internet over tor for enhanced privacy.",
text = stringResource(R.string.about_tor_route),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
@@ -446,14 +447,14 @@ fun AboutSheet(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = "tor Status: $statusText, bootstrap ${torStatus.bootstrapPercent}%",
text = stringResource(R.string.about_tor_status, statusText, torStatus.bootstrapPercent),
style = MaterialTheme.typography.bodySmall,
color = colorScheme.onSurface.copy(alpha = 0.75f)
)
val lastLog = torStatus.lastLogLine
if (lastLog.isNotEmpty()) {
Text(
text = "Last: ${lastLog.take(160)}",
text = stringResource(R.string.about_last, lastLog.take(160)),
style = MaterialTheme.typography.labelSmall,
color = colorScheme.onSurface.copy(alpha = 0.6f)
)
@@ -483,20 +484,20 @@ fun AboutSheet(
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = "Warning",
contentDescription = stringResource(R.string.cd_warning),
tint = errorColor,
modifier = Modifier.size(16.dp)
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = "Emergency Data Deletion",
text = stringResource(R.string.about_emergency_title),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = errorColor
)
Text(
text = "Tip: Triple-click the app title to emergency delete all stored data including messages, keys, and settings.",
text = stringResource(R.string.about_emergency_tip),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.8f)
@@ -523,14 +524,14 @@ fun AboutSheet(
)
) {
Text(
text = "Debug Settings",
text = stringResource(R.string.about_debug_settings),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace
)
}
}
Text(
text = "Open Source • Privacy First • Decentralized",
text = stringResource(R.string.about_footer),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
@@ -557,7 +558,7 @@ fun AboutSheet(
.padding(horizontal = 16.dp)
) {
Text(
text = "Close",
text = stringResource(R.string.close_plain),
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onBackground
)
@@ -588,7 +589,7 @@ fun PasswordPromptDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = "Enter Channel Password",
text = stringResource(R.string.pwd_prompt_title),
style = MaterialTheme.typography.titleMedium,
color = colorScheme.onSurface
)
@@ -596,7 +597,7 @@ fun PasswordPromptDialog(
text = {
Column {
Text(
text = "Channel $channelName is password protected. Enter the password to join.",
text = stringResource(R.string.pwd_prompt_message, channelName ?: ""),
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.onSurface
)
@@ -605,7 +606,7 @@ fun PasswordPromptDialog(
OutlinedTextField(
value = passwordInput,
onValueChange = onPasswordChange,
label = { Text("Password", style = MaterialTheme.typography.bodyMedium) },
label = { Text(stringResource(R.string.pwd_label), style = MaterialTheme.typography.bodyMedium) },
textStyle = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
),
@@ -619,7 +620,7 @@ fun PasswordPromptDialog(
confirmButton = {
TextButton(onClick = onConfirm) {
Text(
text = "Join",
text = stringResource(R.string.join),
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.primary
)
@@ -628,7 +629,7 @@ fun PasswordPromptDialog(
dismissButton = {
TextButton(onClick = onDismiss) {
Text(
text = "Cancel",
text = stringResource(R.string.cancel),
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.onSurface
)
@@ -21,6 +21,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
@@ -64,7 +66,7 @@ fun TorStatusIcon(
}
Icon(
imageVector = Icons.Outlined.Cable,
contentDescription = "Tor status",
contentDescription = stringResource(R.string.cd_tor_status),
modifier = modifier,
tint = cableColor
)
@@ -80,23 +82,23 @@ fun NoiseSessionIcon(
"uninitialized" -> Triple(
Icons.Outlined.NoEncryption,
Color(0x87878700), // Grey - ready to establish
"Ready for handshake"
stringResource(R.string.cd_ready_for_handshake)
)
"handshaking" -> Triple(
Icons.Outlined.Sync,
Color(0x87878700), // Grey - in progress
"Handshake in progress"
stringResource(R.string.cd_handshake_in_progress)
)
"established" -> Triple(
Icons.Filled.Lock,
Color(0xFFFF9500), // Orange - secure
"End-to-end encrypted"
stringResource(R.string.cd_encrypted)
)
else -> { // "failed" or any other state
Triple(
Icons.Outlined.Warning,
Color(0xFFFF4444), // Red - error
"Handshake failed"
stringResource(R.string.cd_handshake_failed)
)
}
}
@@ -129,7 +131,7 @@ fun NicknameEditor(
modifier = modifier
) {
Text(
text = "@",
text = stringResource(R.string.at_symbol),
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.primary.copy(alpha = 0.8f)
)
@@ -193,8 +195,8 @@ fun PeerCounter(
Icon(
imageVector = Icons.Default.Group,
contentDescription = when (selectedLocationChannel) {
is com.bitchat.android.geohash.ChannelID.Location -> "Geohash participants"
else -> "Connected peers"
is com.bitchat.android.geohash.ChannelID.Location -> stringResource(R.string.cd_geohash_participants)
else -> stringResource(R.string.cd_connected_peers)
},
modifier = Modifier.size(16.dp),
tint = countColor
@@ -211,7 +213,7 @@ fun PeerCounter(
if (joinedChannels.isNotEmpty()) {
Text(
text = " · ⧉ ${joinedChannels.size}",
text = stringResource(R.string.channel_count_prefix) + "${joinedChannels.size}",
style = MaterialTheme.typography.bodyMedium,
color = if (isConnected) Color(0xFF00C851) else Color.Red,
fontSize = 16.sp,
@@ -373,13 +375,13 @@ private fun PrivateChatHeader(
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
contentDescription = stringResource(R.string.back),
modifier = Modifier.size(16.dp),
tint = colorScheme.primary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "back",
text = stringResource(R.string.chat_back),
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.primary
)
@@ -405,7 +407,7 @@ private fun PrivateChatHeader(
if (showGlobe) {
Icon(
imageVector = Icons.Outlined.Public,
contentDescription = "Nostr reachable",
contentDescription = stringResource(R.string.cd_nostr_reachable),
modifier = Modifier.size(14.dp),
tint = Color(0xFF9B59B6) // Purple like iOS
)
@@ -428,7 +430,7 @@ private fun PrivateChatHeader(
) {
Icon(
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
contentDescription = if (isFavorite) stringResource(R.string.cd_remove_favorite) else stringResource(R.string.cd_add_favorite),
modifier = Modifier.size(18.dp), // Slightly larger than sidebar icon
tint = if (isFavorite) Color(0xFFFFD700) else Color(0x87878700) // Yellow or grey
)
@@ -463,13 +465,13 @@ private fun ChannelHeader(
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
contentDescription = stringResource(R.string.back),
modifier = Modifier.size(16.dp),
tint = colorScheme.primary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "back",
text = stringResource(R.string.chat_back),
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.primary
)
@@ -478,7 +480,7 @@ private fun ChannelHeader(
// Title - perfectly centered regardless of other elements
Text(
text = "channel: $channel",
text = stringResource(R.string.chat_channel_prefix, channel),
style = MaterialTheme.typography.titleMedium,
color = Color(0xFFFF9500), // Orange to match input field
modifier = Modifier
@@ -492,7 +494,7 @@ private fun ChannelHeader(
modifier = Modifier.align(Alignment.CenterEnd)
) {
Text(
text = "leave",
text = stringResource(R.string.chat_leave),
style = MaterialTheme.typography.bodySmall,
color = Color.Red
)
@@ -534,7 +536,7 @@ private fun MainHeader(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "bitchat/",
text = stringResource(R.string.app_brand),
style = MaterialTheme.typography.headlineSmall,
color = colorScheme.primary,
modifier = Modifier.singleOrTripleClickable(
@@ -562,7 +564,7 @@ private fun MainHeader(
// Render icon directly to avoid symbol resolution issues
Icon(
imageVector = Icons.Filled.Email,
contentDescription = "Unread private messages",
contentDescription = stringResource(R.string.cd_unread_private_messages),
modifier = Modifier
.size(16.dp)
.clickable { viewModel.openLatestUnreadPrivateChat() },
@@ -593,7 +595,7 @@ private fun MainHeader(
) {
Icon(
imageVector = if (isBookmarked) Icons.Filled.Bookmark else Icons.Outlined.BookmarkBorder,
contentDescription = "Toggle bookmark",
contentDescription = stringResource(R.string.cd_toggle_bookmark),
tint = if (isBookmarked) Color(0xFF00C851) else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f),
modifier = Modifier.size(16.dp)
)
@@ -668,7 +670,7 @@ private fun LocationChannelsButton(
Spacer(modifier = Modifier.width(2.dp))
Icon(
imageVector = Icons.Default.PinDrop,
contentDescription = "Teleported",
contentDescription = stringResource(R.string.cd_teleported),
modifier = Modifier.size(12.dp),
tint = badgeColor
)
@@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.IconButton
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
@@ -294,7 +295,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
IconButton(onClick = { forceScrollToBottom = !forceScrollToBottom }) {
Icon(
imageVector = Icons.Filled.ArrowDownward,
contentDescription = "Scroll to bottom",
contentDescription = stringResource(com.bitchat.android.R.string.cd_scroll_to_bottom),
tint = Color(0xFF00C851)
)
}
@@ -12,6 +12,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import kotlinx.coroutines.launch
@@ -61,7 +63,7 @@ fun ChatUserSheet(
) {
// Header
Text(
text = "@$targetNickname",
text = stringResource(R.string.at_nickname, targetNickname),
fontSize = 18.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
@@ -69,7 +71,7 @@ fun ChatUserSheet(
)
Text(
text = if (selectedMessage != null) "choose an action for this message or user" else "choose an action for this user",
text = if (selectedMessage != null) stringResource(R.string.choose_action_message_or_user) else stringResource(R.string.choose_action_user),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
@@ -83,8 +85,8 @@ fun ChatUserSheet(
selectedMessage?.let { message ->
item {
UserActionRow(
title = "copy message",
subtitle = "copy this message to clipboard",
title = stringResource(R.string.action_copy_message_title),
subtitle = stringResource(R.string.action_copy_message_subtitle),
titleColor = standardGrey,
onClick = {
// Copy the message content to clipboard
@@ -100,8 +102,8 @@ fun ChatUserSheet(
// Slap action
item {
UserActionRow(
title = "slap $targetNickname",
subtitle = "send a playful slap message",
title = stringResource(R.string.action_slap_title, targetNickname),
subtitle = stringResource(R.string.action_slap_subtitle),
titleColor = standardBlue,
onClick = {
// Send slap command
@@ -114,8 +116,8 @@ fun ChatUserSheet(
// Hug action
item {
UserActionRow(
title = "hug $targetNickname",
subtitle = "send a friendly hug message",
title = stringResource(R.string.action_hug_title, targetNickname),
subtitle = stringResource(R.string.action_hug_subtitle),
titleColor = standardGreen,
onClick = {
// Send hug command
@@ -128,8 +130,8 @@ fun ChatUserSheet(
// Block action
item {
UserActionRow(
title = "block $targetNickname",
subtitle = "block all messages from this user",
title = stringResource(R.string.action_block_title, targetNickname),
subtitle = stringResource(R.string.action_block_subtitle),
titleColor = standardRed,
onClick = {
// Check if we're in a geohash channel
@@ -158,7 +160,7 @@ fun ChatUserSheet(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "cancel",
text = stringResource(R.string.cancel_lower),
fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace
)
@@ -226,20 +226,6 @@ class ChatViewModel(
} catch (_: Exception) { }
// Note: Mesh service is now started by MainActivity
// Show welcome message if no peers after delay
viewModelScope.launch {
delay(10000)
if (state.getConnectedPeersValue().isEmpty() && state.getMessagesValue().isEmpty()) {
val welcomeMessage = BitchatMessage(
sender = "system",
content = "get people around you to download bitchat and chat with them here!",
timestamp = Date(),
isRelay = false
)
messageManager.addMessage(welcomeMessage)
}
}
// BLE receives are inserted by MessageHandler path; no VoiceNoteBus for Tor in this branch.
}
@@ -20,6 +20,8 @@ import androidx.compose.ui.unit.sp
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import java.util.*
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
/**
* GeohashPeopleList - iOS-compatible component for displaying geohash participants
@@ -66,7 +68,7 @@ fun GeohashPeopleList(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "PEOPLE",
text = stringResource(R.string.geohash_people_header),
style = MaterialTheme.typography.labelSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
@@ -78,7 +80,7 @@ fun GeohashPeopleList(
if (geohashPeople.isEmpty()) {
// Empty state - matches iOS "nobody around..."
Text(
text = "nobody around...",
text = stringResource(R.string.nobody_around),
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontSize = BASE_FONT_SIZE.sp
@@ -182,7 +184,7 @@ private fun GeohashPersonItem(
// Unread DM indicator (orange envelope)
Icon(
imageVector = Icons.Filled.Email,
contentDescription = "Unread message",
contentDescription = stringResource(R.string.cd_unread_message),
modifier = Modifier.size(12.dp),
tint = Color(0xFFFF9500) // iOS orange
)
@@ -253,7 +255,7 @@ private fun GeohashPersonItem(
// "You" indicator for current user
if (isMe) {
Text(
text = " (you)",
text = stringResource(R.string.you_suffix),
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontSize = BASE_FONT_SIZE.sp
@@ -37,6 +37,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import androidx.core.view.updateLayoutParams
import com.bitchat.android.geohash.Geohash
import com.bitchat.android.geohash.LocationChannelManager
@@ -175,7 +177,7 @@ class GeohashPickerActivity : ComponentActivity() {
shadowElevation = 6.dp
) {
Text(
text = "pan and zoom to select a geohash",
text = stringResource(R.string.pan_zoom_instruction),
fontSize = 12.sp,
textAlign = TextAlign.Center,
fontFamily = FontFamily.Monospace,
@@ -228,7 +230,7 @@ class GeohashPickerActivity : ComponentActivity() {
)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Remove, contentDescription = "Decrease precision")
Icon(Icons.Filled.Remove, contentDescription = stringResource(R.string.cd_decrease_precision))
}
}
@@ -244,7 +246,7 @@ class GeohashPickerActivity : ComponentActivity() {
)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Add, contentDescription = "Increase precision")
Icon(Icons.Filled.Add, contentDescription = stringResource(R.string.cd_increase_precision))
}
}
@@ -264,10 +266,10 @@ class GeohashPickerActivity : ComponentActivity() {
)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Check, contentDescription = "Select geohash")
Icon(Icons.Filled.Check, contentDescription = stringResource(R.string.cd_select_geohash))
Spacer(Modifier.width(6.dp))
Text(
text = "select",
text = stringResource(R.string.select),
fontSize = (BASE_FONT_SIZE - 2).sp,
fontFamily = FontFamily.Monospace
)
@@ -217,7 +217,7 @@ fun MessageInput(
// Show placeholder when there's no text and not recording
if (value.text.isEmpty() && !isRecording) {
Text(
text = "type a message...",
text = stringResource(R.string.type_a_message_placeholder),
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
),
@@ -486,7 +486,7 @@ fun MentionSuggestionItem(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "@$suggestion",
text = stringResource(R.string.mention_suggestion_at, suggestion),
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.SemiBold
@@ -498,7 +498,7 @@ fun MentionSuggestionItem(
Spacer(modifier = Modifier.weight(1f))
Text(
text = "mention",
text = stringResource(R.string.mention),
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace
),
@@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -94,7 +95,7 @@ fun LinkPreviewPill(
) {
Icon(
imageVector = Icons.Outlined.Link,
contentDescription = "Link",
contentDescription = stringResource(com.bitchat.android.R.string.cd_link),
modifier = Modifier.size(24.dp),
tint = Color.Blue
)
@@ -37,6 +37,8 @@ import com.bitchat.android.geohash.GeohashChannelLevel
import com.bitchat.android.geohash.LocationChannelManager
import com.bitchat.android.geohash.GeohashBookmarksStore
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
/**
* Location Channels Sheet for selecting geohash-based location channels
@@ -133,7 +135,7 @@ fun LocationChannelsSheet(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "#location channels",
text = stringResource(R.string.location_channels_title),
style = MaterialTheme.typography.headlineSmall,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
@@ -141,7 +143,7 @@ fun LocationChannelsSheet(
)
Text(
text = "chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps. do not screenshot or share this screen to protect your privacy.",
text = stringResource(R.string.location_channels_desc),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
@@ -170,7 +172,7 @@ fun LocationChannelsSheet(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "grant location permission",
text = stringResource(R.string.grant_location_permission),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace
)
@@ -180,7 +182,7 @@ fun LocationChannelsSheet(
LocationChannelManager.PermissionState.RESTRICTED -> {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = "location permission denied. enable in settings to use location channels.",
text = stringResource(R.string.location_permission_denied),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = Color.Red.copy(alpha = 0.8f)
@@ -194,7 +196,7 @@ fun LocationChannelsSheet(
}
) {
Text(
text = "open settings",
text = stringResource(R.string.open_settings),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace
)
@@ -203,7 +205,7 @@ fun LocationChannelsSheet(
}
LocationChannelManager.PermissionState.AUTHORIZED -> {
Text(
text = "✓ location permission granted",
text = stringResource(R.string.location_permission_granted),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = standardGreen
@@ -216,7 +218,7 @@ fun LocationChannelsSheet(
) {
CircularProgressIndicator(modifier = Modifier.size(12.dp))
Text(
text = "checking permissions...",
text = stringResource(R.string.checking_permissions),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
@@ -232,7 +234,7 @@ fun LocationChannelsSheet(
item(key = "mesh") {
ChannelRow(
title = meshTitleWithCount(viewModel),
subtitle = "#bluetooth • ${bluetoothRangeString()}",
subtitle = stringResource(R.string.location_bluetooth_subtitle, bluetoothRangeString()),
isSelected = selectedChannel is ChannelID.Mesh,
titleColor = standardBlue,
titleBold = meshCount(viewModel) > 0,
@@ -262,13 +264,13 @@ fun LocationChannelsSheet(
titleColor = standardGreen,
titleBold = highlight,
trailingContent = {
IconButton(onClick = { bookmarksStore.toggle(channel.geohash) }) {
Icon(
imageVector = if (isBookmarked) Icons.Filled.Bookmark else Icons.Outlined.BookmarkBorder,
contentDescription = if (isBookmarked) "Unbookmark" else "Bookmark",
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
)
}
IconButton(onClick = { bookmarksStore.toggle(channel.geohash) }) {
Icon(
imageVector = if (isBookmarked) Icons.Filled.Bookmark else Icons.Outlined.BookmarkBorder,
contentDescription = if (isBookmarked) stringResource(R.string.cd_remove_bookmark) else stringResource(R.string.cd_add_bookmark),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
)
}
},
onClick = {
// Selecting a suggested nearby channel is not a teleport
@@ -286,7 +288,7 @@ fun LocationChannelsSheet(
) {
CircularProgressIndicator(modifier = Modifier.size(16.dp))
Text(
text = "finding nearby channels…",
text = stringResource(R.string.finding_nearby_channels),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace
)
@@ -298,7 +300,7 @@ fun LocationChannelsSheet(
if (bookmarks.isNotEmpty()) {
item(key = "bookmarked_header") {
Text(
text = "bookmarked",
text = stringResource(R.string.bookmarked),
style = MaterialTheme.typography.labelLarge,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
@@ -328,7 +330,7 @@ fun LocationChannelsSheet(
IconButton(onClick = { bookmarksStore.toggle(gh) }) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = "Remove bookmark",
contentDescription = stringResource(R.string.cd_remove_bookmark),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
)
}
@@ -366,7 +368,7 @@ fun LocationChannelsSheet(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "#",
text = stringResource(R.string.hash_symbol),
fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
@@ -409,7 +411,7 @@ fun LocationChannelsSheet(
decorationBox = { innerTextField ->
if (customGeohash.isEmpty()) {
Text(
text = "geohash",
text = stringResource(R.string.geohash_placeholder),
fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
@@ -435,7 +437,7 @@ fun LocationChannelsSheet(
}) {
Icon(
imageVector = Icons.Filled.Map,
contentDescription = "Open map",
contentDescription = stringResource(R.string.cd_open_map),
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
)
}
@@ -453,7 +455,7 @@ fun LocationChannelsSheet(
locationManager.select(ChannelID.Location(channel))
onDismiss()
} else {
customError = "invalid geohash"
customError = context.getString(R.string.invalid_geohash)
}
},
enabled = isValid,
@@ -467,14 +469,13 @@ fun LocationChannelsSheet(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "teleport",
text = stringResource(R.string.teleport),
fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace
)
// iOS has a face.dashed icon, use closest Material equivalent
Icon(
imageVector = Icons.Filled.PinDrop,
contentDescription = "Teleport",
contentDescription = stringResource(R.string.cd_teleport),
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurface
)
@@ -530,11 +531,7 @@ fun LocationChannelsSheet(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = if (locationServicesEnabled) {
"disable location services"
} else {
"enable location services"
},
text = if (locationServicesEnabled) stringResource(R.string.disable_location_services) else stringResource(R.string.enable_location_services),
fontSize = 12.sp,
fontFamily = FontFamily.Monospace
)
@@ -558,7 +555,7 @@ fun LocationChannelsSheet(
.padding(horizontal = 16.dp)
) {
Text(
text = "Close",
text = stringResource(R.string.close_plain),
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onBackground
)
@@ -668,7 +665,7 @@ private fun ChannelRow(
if (isSelected) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Selected",
contentDescription = stringResource(R.string.cd_selected),
tint = Color(0xFF32D74B), // iOS green for checkmark
modifier = Modifier.size(20.dp)
)
@@ -695,10 +692,13 @@ private fun splitTitleAndCount(title: String): Pair<String, String?> {
}
}
@Composable
private fun meshTitleWithCount(viewModel: ChatViewModel): String {
val meshCount = meshCount(viewModel)
val noun = if (meshCount == 1) "person" else "people"
return "mesh [$meshCount $noun]"
val ctx = androidx.compose.ui.platform.LocalContext.current
val peopleText = ctx.resources.getQuantityString(com.bitchat.android.R.plurals.people_count, meshCount, meshCount)
val meshLabel = stringResource(com.bitchat.android.R.string.mesh_label)
return "$meshLabel [$peopleText]"
}
private fun meshCount(viewModel: ChatViewModel): Int {
@@ -708,14 +708,25 @@ private fun meshCount(viewModel: ChatViewModel): Int {
} ?: 0
}
@Composable
private fun geohashTitleWithCount(channel: GeohashChannel, participantCount: Int): String {
val noun = if (participantCount == 1) "person" else "people"
return "${channel.level.displayName.lowercase()} [$participantCount $noun]"
val ctx = androidx.compose.ui.platform.LocalContext.current
val peopleText = ctx.resources.getQuantityString(com.bitchat.android.R.plurals.people_count, participantCount, participantCount)
val levelName = when (channel.level) {
com.bitchat.android.geohash.GeohashChannelLevel.BLOCK -> stringResource(com.bitchat.android.R.string.location_level_block)
com.bitchat.android.geohash.GeohashChannelLevel.NEIGHBORHOOD -> stringResource(com.bitchat.android.R.string.location_level_neighborhood)
com.bitchat.android.geohash.GeohashChannelLevel.CITY -> stringResource(com.bitchat.android.R.string.location_level_city)
com.bitchat.android.geohash.GeohashChannelLevel.PROVINCE -> stringResource(com.bitchat.android.R.string.location_level_province)
com.bitchat.android.geohash.GeohashChannelLevel.REGION -> stringResource(com.bitchat.android.R.string.location_level_region)
}
return "$levelName [$peopleText]"
}
@Composable
private fun geohashHashTitleWithCount(geohash: String, participantCount: Int): String {
val noun = if (participantCount == 1) "person" else "people"
return "#$geohash [$participantCount $noun]"
val ctx = androidx.compose.ui.platform.LocalContext.current
val peopleText = ctx.resources.getQuantityString(com.bitchat.android.R.plurals.people_count, participantCount, participantCount)
return "#$geohash [$peopleText]"
}
private fun isChannelSelected(channel: GeohashChannel, selectedChannel: ChannelID?): Boolean {
@@ -43,6 +43,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.shape.CircleShape
import com.bitchat.android.ui.media.FileMessageItem
import com.bitchat.android.model.BitchatMessageType
import com.bitchat.android.R
import androidx.compose.ui.res.stringResource
// VoiceNotePlayer moved to com.bitchat.android.ui.media.VoiceNotePlayer
@@ -334,11 +337,11 @@ fun MessageItem(
.clickable { onCancelTransfer?.invoke(message) },
contentAlignment = Alignment.Center
) {
Icon(imageVector = Icons.Filled.Close, contentDescription = "Cancel", tint = Color.White, modifier = Modifier.size(14.dp))
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(R.string.cd_cancel), tint = Color.White, modifier = Modifier.size(14.dp))
}
}
} else {
Text(text = "[file unavailable]", fontFamily = FontFamily.Monospace, color = Color.Gray)
Text(text = stringResource(R.string.file_unavailable), fontFamily = FontFamily.Monospace, color = Color.Gray)
}
}
}
@@ -471,7 +474,7 @@ fun DeliveryStatusIcon(status: DeliveryStatus) {
when (status) {
is DeliveryStatus.Sending -> {
Text(
text = "",
text = stringResource(R.string.status_sending),
fontSize = 10.sp,
color = colorScheme.primary.copy(alpha = 0.6f)
)
@@ -479,7 +482,7 @@ fun DeliveryStatusIcon(status: DeliveryStatus) {
is DeliveryStatus.Sent -> {
// Use a subtle hollow marker for Sent; single check is reserved for Delivered (iOS parity)
Text(
text = "",
text = stringResource(R.string.status_pending),
fontSize = 10.sp,
color = colorScheme.primary.copy(alpha = 0.6f)
)
@@ -487,14 +490,14 @@ fun DeliveryStatusIcon(status: DeliveryStatus) {
is DeliveryStatus.Delivered -> {
// Single check for Delivered (matches iOS expectations)
Text(
text = "",
text = stringResource(R.string.status_sent),
fontSize = 10.sp,
color = colorScheme.primary.copy(alpha = 0.8f)
)
}
is DeliveryStatus.Read -> {
Text(
text = "✓✓",
text = stringResource(R.string.status_delivered),
fontSize = 10.sp,
color = Color(0xFF007AFF), // Blue
fontWeight = FontWeight.Bold
@@ -502,7 +505,7 @@ fun DeliveryStatusIcon(status: DeliveryStatus) {
}
is DeliveryStatus.Failed -> {
Text(
text = "",
text = stringResource(R.string.status_failed),
fontSize = 10.sp,
color = Color.Red.copy(alpha = 0.8f)
)
@@ -510,7 +513,7 @@ fun DeliveryStatusIcon(status: DeliveryStatus) {
is DeliveryStatus.PartiallyDelivered -> {
// Show a single subdued check without numeric label
Text(
text = "",
text = stringResource(R.string.status_sent),
fontSize = 10.sp,
color = colorScheme.primary.copy(alpha = 0.6f)
)
@@ -258,7 +258,10 @@ class NotificationManager(
}
if (messageCount > 5) {
style.setSummaryText("and ${messageCount - 5} more")
val extra = messageCount - 5
style.setSummaryText(context.resources.getQuantityString(
R.plurals.notification_and_more, extra, extra
))
}
builder.setStyle(style)
@@ -291,11 +294,11 @@ class NotificationManager(
)
// Build notification content
val contentTitle = "👥 bitchatters nearby!"
val contentTitle = context.getString(R.string.notification_active_peers_title)
val contentText = if (peersSize == 1) {
"1 person around"
context.getString(R.string.notification_active_peers_one)
} else {
"$peersSize people around"
context.getString(R.string.notification_active_peers_many, peersSize)
}
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
@@ -331,8 +334,8 @@ class NotificationManager(
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("bitchat")
.setContentText("$totalMessages messages from $senderCount people")
.setContentTitle(context.getString(R.string.app_name))
.setContentText(context.getString(R.string.notification_messages_from_people, totalMessages, senderCount))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
@@ -342,7 +345,7 @@ class NotificationManager(
// Add inbox style showing recent senders
val style = NotificationCompat.InboxStyle()
.setBigContentTitle("New Messages")
.setBigContentTitle(context.getString(R.string.notification_new_location_messages))
pendingNotifications.entries.take(5).forEach { (peerID, notifications) ->
val latestNotif = notifications.last()
@@ -356,7 +359,7 @@ class NotificationManager(
}
if (pendingNotifications.size > 5) {
style.setSummaryText("and ${pendingNotifications.size - 5} more conversations")
style.setSummaryText(context.getString(R.string.notification_more_conversations, pendingNotifications.size - 5))
}
builder.setStyle(style)
@@ -459,15 +462,15 @@ class NotificationManager(
// Build notification content with location name if available
val geohashDisplay = latestNotification.locationName?.let { "$it (#$geohash)" } ?: "#$geohash"
val contentTitle = when {
mentionCount > 0 && firstMessageCount > 0 && messageCount > 1 -> "Mentioned in $geohashDisplay (+${messageCount - 1} more)"
mentionCount > 0 -> if (mentionCount == 1) "Mentioned in $geohashDisplay" else "$mentionCount mentions in $geohashDisplay"
firstMessageCount > 0 -> "New activity in $geohashDisplay"
else -> "Messages in $geohashDisplay"
mentionCount > 0 && firstMessageCount > 0 && messageCount > 1 -> context.getString(R.string.notification_mentions_in_more, geohashDisplay, messageCount - 1)
mentionCount > 0 -> if (mentionCount == 1) context.getString(R.string.notification_mentions_in, geohashDisplay) else context.getString(R.string.notification_mentions_in_plural, mentionCount, geohashDisplay)
firstMessageCount > 0 -> context.getString(R.string.notification_new_activity_in, geohashDisplay)
else -> context.getString(R.string.notification_messages_in, geohashDisplay)
}
val contentText = when {
latestNotification.isMention -> "${latestNotification.senderNickname}: ${latestNotification.messageContent}"
latestNotification.isFirstMessage -> "${latestNotification.senderNickname} joined the conversation"
latestNotification.isFirstMessage -> context.getString(R.string.notification_joined_conversation, latestNotification.senderNickname)
else -> "${latestNotification.senderNickname}: ${latestNotification.messageContent}"
}
@@ -503,7 +506,8 @@ class NotificationManager(
}
if (messageCount > 5) {
style.setSummaryText("and ${messageCount - 5} more")
val extra = messageCount - 5
style.setSummaryText(context.resources.getQuantityString(R.plurals.notification_and_more, extra, extra))
}
builder.setStyle(style)
@@ -543,12 +547,12 @@ class NotificationManager(
)
val contentTitle = if (totalMentions > 0) {
"bitchat - $totalMentions mentions"
context.getString(R.string.notification_geohash_summary_title_mentions, totalMentions)
} else {
"bitchat - location chats"
context.getString(R.string.notification_geohash_summary_title)
}
val contentText = "$totalMessages messages from $geohashCount locations"
val contentText = context.getString(R.string.notification_geohash_summary_text, totalMessages, geohashCount)
val builder = NotificationCompat.Builder(context, GEOHASH_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
@@ -563,7 +567,7 @@ class NotificationManager(
// Add inbox style showing recent geohashes
val style = NotificationCompat.InboxStyle()
.setBigContentTitle("New Location Messages")
.setBigContentTitle(context.getString(R.string.notification_new_messages))
pendingGeohashNotifications.entries.take(5).forEach { (geohash, notifications) ->
val mentionCount = notifications.count { it.isMention }
@@ -579,7 +583,7 @@ class NotificationManager(
}
if (pendingGeohashNotifications.size > 5) {
style.setSummaryText("and ${pendingGeohashNotifications.size - 5} more locations")
style.setSummaryText(context.getString(R.string.notification_more_locations, pendingGeohashNotifications.size - 5))
}
builder.setStyle(style)
@@ -676,9 +680,9 @@ class NotificationManager(
// Build notification content
val contentTitle = if (messageCount == 1) {
"Mentioned in Mesh Chat"
context.getString(R.string.notification_mesh_mention_title_singular)
} else {
"$messageCount mentions in Mesh Chat"
context.getString(R.string.notification_mesh_mention_title_plural, messageCount)
}
val contentText = "${latestNotification.senderNickname}: ${latestNotification.messageContent}"
@@ -710,7 +714,8 @@ class NotificationManager(
}
if (messageCount > 5) {
style.setSummaryText("and ${messageCount - 5} more")
val extra = messageCount - 5
style.setSummaryText(context.resources.getQuantityString(R.plurals.notification_and_more, extra, extra))
}
builder.setStyle(style)
@@ -15,6 +15,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.nostr.NostrProofOfWork
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import com.bitchat.android.nostr.PoWPreferenceManager
/**
@@ -54,7 +56,7 @@ fun PoWStatusIndicator(
Icon(
imageVector = Icons.Filled.Security,
contentDescription = "Mining PoW",
contentDescription = stringResource(R.string.cd_mining_pow),
tint = Color(0xFFFF9500), // Orange for mining
modifier = Modifier
.size(12.dp)
@@ -63,7 +65,7 @@ fun PoWStatusIndicator(
} else {
Icon(
imageVector = Icons.Filled.Security,
contentDescription = "PoW Enabled",
contentDescription = stringResource(R.string.cd_pow_enabled),
tint = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D), // Green when ready
modifier = Modifier.size(12.dp)
)
@@ -85,7 +87,7 @@ fun PoWStatusIndicator(
// PoW icon
Icon(
imageVector = Icons.Filled.Security,
contentDescription = "Proof of Work",
contentDescription = stringResource(R.string.cd_proof_of_work),
tint = if (isMining) Color(0xFFFF9500) else {
if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
},
@@ -95,9 +97,9 @@ fun PoWStatusIndicator(
// Status text
Text(
text = if (isMining) {
"mining..."
stringResource(R.string.pow_mining_ellipsis)
} else {
"pow: ${powDifficulty}bit"
stringResource(R.string.pow_label_format, powDifficulty)
},
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
@@ -109,7 +111,7 @@ fun PoWStatusIndicator(
// Time estimate
if (!isMining && powDifficulty > 0) {
Text(
text = "(~${NostrProofOfWork.estimateMiningTime(powDifficulty)})",
text = stringResource(R.string.pow_time_estimate, NostrProofOfWork.estimateMiningTime(powDifficulty)),
fontSize = 9.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.5f)
@@ -237,7 +237,7 @@ fun ChannelsSection(
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Leave channel",
contentDescription = stringResource(com.bitchat.android.R.string.cd_leave_channel),
modifier = Modifier.size(14.dp),
tint = colorScheme.onSurface.copy(alpha = 0.5f)
)
@@ -552,7 +552,7 @@ private fun PeerItem(
// Show mail icon for unread DMs (iOS orange)
Icon(
imageVector = Icons.Filled.Email,
contentDescription = "Unread message",
contentDescription = stringResource(com.bitchat.android.R.string.cd_unread_message),
modifier = Modifier.size(16.dp),
tint = Color(0xFFFF9500) // iOS orange
)
@@ -562,7 +562,7 @@ private fun PeerItem(
// Purple globe to indicate Nostr availability
Icon(
imageVector = Icons.Filled.Public,
contentDescription = "Reachable via Nostr",
contentDescription = stringResource(com.bitchat.android.R.string.cd_reachable_via_nostr),
modifier = Modifier.size(16.dp),
tint = Color(0xFF9C27B0) // Purple
)
@@ -571,7 +571,7 @@ private fun PeerItem(
Icon(
//painter = androidx.compose.ui.res.painterResource(id = R.drawable.ic_offline_favorite),
imageVector = Icons.Outlined.Circle,
contentDescription = "Offline favorite",
contentDescription = stringResource(com.bitchat.android.R.string.cd_offline_favorite),
modifier = Modifier.size(16.dp),
tint = Color.Gray
)
@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.unit.dp
@@ -129,7 +130,7 @@ fun VoiceRecordButton(
) {
Icon(
imageVector = Icons.Filled.Mic,
contentDescription = "Record voice note",
contentDescription = stringResource(com.bitchat.android.R.string.cd_record_voice),
tint = Color.Black,
modifier = Modifier.size(20.dp)
)
@@ -25,6 +25,8 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.draw.rotate
import com.bitchat.android.mesh.BluetoothMeshService
import kotlinx.coroutines.launch
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -97,10 +99,10 @@ fun DebugSettingsSheet(
item {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.BugReport, contentDescription = null, tint = Color(0xFFFF9500))
Text("debug tools", fontFamily = FontFamily.Monospace, fontSize = 18.sp, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.debug_tools), fontFamily = FontFamily.Monospace, fontSize = 18.sp, fontWeight = FontWeight.Medium)
}
Text(
text = "developer utilities for diagnostics and control",
text = stringResource(R.string.debug_tools_desc),
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
color = colorScheme.onSurface.copy(alpha = 0.7f)
@@ -113,12 +115,12 @@ fun DebugSettingsSheet(
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.SettingsEthernet, contentDescription = null, tint = Color(0xFF00C851))
Text("verbose logging", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.debug_verbose_logging), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Spacer(Modifier.weight(1f))
Switch(checked = verboseLogging, onCheckedChange = { manager.setVerboseLoggingEnabled(it) })
}
Text(
"logs peer joins/leaves, connection direction, packet routing and relays",
stringResource(R.string.debug_verbose_hint),
fontFamily = FontFamily.Monospace,
fontSize = 11.sp,
color = colorScheme.onSurface.copy(alpha = 0.7f)
@@ -133,10 +135,10 @@ fun DebugSettingsSheet(
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.Bluetooth, contentDescription = null, tint = Color(0xFF007AFF))
Text("bluetooth roles", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.debug_bluetooth_roles), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("gatt server", fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
Text(stringResource(R.string.debug_gatt_server), fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
Switch(checked = gattServerEnabled, onCheckedChange = {
manager.setGattServerEnabled(it)
scope.launch {
@@ -145,9 +147,9 @@ fun DebugSettingsSheet(
})
}
val serverCount = connectedDevices.count { it.connectionType == ConnectionType.GATT_SERVER }
Text("connections: $serverCount / $maxServer", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text(stringResource(R.string.debug_connections_fmt, serverCount, maxServer), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Row(verticalAlignment = Alignment.CenterVertically) {
Text("max server", fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
Text(stringResource(R.string.debug_max_server), fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
Slider(
value = maxServer.toFloat(),
onValueChange = { manager.setMaxServerConnections(it.toInt().coerceAtLeast(1)) },
@@ -156,7 +158,7 @@ fun DebugSettingsSheet(
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("gatt client", fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
Text(stringResource(R.string.debug_gatt_client), fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
Switch(checked = gattClientEnabled, onCheckedChange = {
manager.setGattClientEnabled(it)
scope.launch {
@@ -165,9 +167,9 @@ fun DebugSettingsSheet(
})
}
val clientCount = connectedDevices.count { it.connectionType == ConnectionType.GATT_CLIENT }
Text("connections: $clientCount / $maxClient", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text(stringResource(R.string.debug_connections_fmt, clientCount, maxClient), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Row(verticalAlignment = Alignment.CenterVertically) {
Text("max client", fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
Text(stringResource(R.string.debug_max_client), fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
Slider(
value = maxClient.toFloat(),
onValueChange = { manager.setMaxClientConnections(it.toInt().coerceAtLeast(1)) },
@@ -176,9 +178,9 @@ fun DebugSettingsSheet(
)
}
val overallCount = connectedDevices.size
Text("connections: $overallCount / $maxOverall", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text(stringResource(R.string.debug_overall_connections_fmt, overallCount, maxOverall), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Row(verticalAlignment = Alignment.CenterVertically) {
Text("max overall", fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
Text(stringResource(R.string.debug_max_overall), fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
Slider(
value = maxOverall.toFloat(),
onValueChange = { manager.setMaxConnectionsOverall(it.toInt().coerceAtLeast(1)) },
@@ -187,7 +189,7 @@ fun DebugSettingsSheet(
)
}
Text(
"turn roles on/off and close all connections when disabled",
stringResource(R.string.debug_roles_hint),
fontFamily = FontFamily.Monospace,
fontSize = 11.sp,
color = colorScheme.onSurface.copy(alpha = 0.7f)
@@ -202,12 +204,12 @@ fun DebugSettingsSheet(
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.PowerSettingsNew, contentDescription = null, tint = Color(0xFFFF9500))
Text("packet relay", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.debug_packet_relay), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Spacer(Modifier.weight(1f))
Switch(checked = packetRelayEnabled, onCheckedChange = { manager.setPacketRelayEnabled(it) })
}
Text("since start: ${relayStats.totalRelaysCount}", fontFamily = FontFamily.Monospace, fontSize = 11.sp)
Text("last 10s: ${relayStats.last10SecondRelays} • 1m: ${relayStats.lastMinuteRelays} • 15m: ${relayStats.last15MinuteRelays}", fontFamily = FontFamily.Monospace, fontSize = 11.sp)
Text(stringResource(R.string.debug_since_start_fmt, relayStats.totalRelaysCount), fontFamily = FontFamily.Monospace, fontSize = 11.sp)
Text(stringResource(R.string.debug_relays_window_fmt, relayStats.last10SecondRelays, relayStats.lastMinuteRelays, relayStats.last15MinuteRelays), fontFamily = FontFamily.Monospace, fontSize = 11.sp)
// Realtime graph: per-second relays, full-width canvas, bottom-up bars, fast decay
var series by remember { mutableStateOf(List(60) { 0f }) }
LaunchedEffect(isPresented) {
@@ -295,15 +297,15 @@ fun DebugSettingsSheet(
Icon(Icons.Filled.SettingsEthernet, contentDescription = null, tint = Color(0xFF9C27B0))
Text("sync settings", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
Text("max packets per sync: $seenCapacity", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text(stringResource(R.string.debug_max_packets_per_sync_fmt, seenCapacity), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Slider(value = seenCapacity.toFloat(), onValueChange = { manager.setSeenPacketCapacity(it.toInt()) }, valueRange = 10f..1000f, steps = 99)
Text("max GCS filter size: $gcsMaxBytes bytes (1281024)", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text(stringResource(R.string.debug_max_gcs_filter_size_fmt, gcsMaxBytes), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Slider(value = gcsMaxBytes.toFloat(), onValueChange = { manager.setGcsMaxBytes(it.toInt()) }, valueRange = 128f..1024f, steps = 0)
Text("target FPR: ${String.format("%.2f", gcsFpr)}%", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text(stringResource(R.string.debug_target_fpr_fmt, String.format("%.2f", gcsFpr)), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Slider(value = gcsFpr.toFloat(), onValueChange = { manager.setGcsFprPercent(it.toDouble()) }, valueRange = 0.1f..5.0f, steps = 49)
val p = remember(gcsFpr) { com.bitchat.android.sync.GCSFilter.deriveP(gcsFpr / 100.0) }
val nmax = remember(gcsFpr, gcsMaxBytes) { com.bitchat.android.sync.GCSFilter.estimateMaxElementsForSize(gcsMaxBytes, p) }
Text("derived P: $p • est. max elements: $nmax", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text(stringResource(R.string.debug_derived_p_fmt, p, nmax), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
}
}
}
@@ -314,10 +316,10 @@ fun DebugSettingsSheet(
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.Devices, contentDescription = null, tint = Color(0xFF4CAF50))
Text("connected devices", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.debug_connected_devices), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
val localAddr = remember { meshService.connectionManager.getLocalAdapterAddress() }
Text("our device id: ${localAddr ?: "unknown"}", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text(stringResource(R.string.debug_our_device_id_fmt, localAddr ?: stringResource(R.string.unknown)), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
if (connectedDevices.isEmpty()) {
Text("none", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f))
} else {
@@ -325,11 +327,11 @@ fun DebugSettingsSheet(
Surface(shape = RoundedCornerShape(8.dp), color = colorScheme.surface.copy(alpha = 0.6f)) {
Row(Modifier.fillMaxWidth().padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text("${dev.peerID ?: "unknown"}${dev.deviceAddress}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
val roleLabel = if (dev.connectionType == ConnectionType.GATT_SERVER) "as server (we host)" else "as client (we connect)"
Text("${dev.nickname ?: ""}RSSI: ${dev.rssi ?: "?"}$roleLabel${if (dev.isDirectConnection) " • direct" else ""}", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text((dev.peerID ?: stringResource(R.string.unknown)) + "${dev.deviceAddress}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
val roleLabel = if (dev.connectionType == ConnectionType.GATT_SERVER) stringResource(R.string.debug_role_server) else stringResource(R.string.debug_role_client)
Text("${dev.nickname ?: ""}" + stringResource(R.string.debug_rssi_fmt, dev.rssi ?: stringResource(R.string.debug_question_mark)) + "$roleLabel" + (if (dev.isDirectConnection) stringResource(R.string.debug_direct_suffix) else ""), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
}
Text("disconnect", color = Color(0xFFBF1A1A), fontFamily = FontFamily.Monospace, modifier = Modifier.clickable {
Text(stringResource(R.string.debug_disconnect), color = Color(0xFFBF1A1A), fontFamily = FontFamily.Monospace, modifier = Modifier.clickable {
meshService.connectionManager.disconnectAddress(dev.deviceAddress)
})
}
@@ -346,19 +348,19 @@ fun DebugSettingsSheet(
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.Bluetooth, contentDescription = null, tint = Color(0xFF007AFF))
Text("recent scan results", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.debug_recent_scan_results), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
if (scanResults.isEmpty()) {
Text("none", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f))
Text(stringResource(R.string.debug_none), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f))
} else {
scanResults.forEach { res ->
Surface(shape = RoundedCornerShape(8.dp), color = colorScheme.surface.copy(alpha = 0.6f)) {
Row(Modifier.fillMaxWidth().padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text("${res.peerID ?: "unknown"}${res.deviceAddress}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
Text("${res.deviceName ?: ""} • RSSI: ${res.rssi}", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Text((res.peerID ?: stringResource(R.string.unknown)) + "${res.deviceAddress}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
Text(stringResource(R.string.debug_rssi_fmt, res.rssi.toString()), fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
}
Text("connect", color = Color(0xFF00C851), fontFamily = FontFamily.Monospace, modifier = Modifier.clickable {
Text(stringResource(R.string.debug_connect), color = Color(0xFF00C851), fontFamily = FontFamily.Monospace, modifier = Modifier.clickable {
meshService.connectionManager.connectToAddress(res.deviceAddress)
})
}
@@ -375,9 +377,9 @@ fun DebugSettingsSheet(
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.BugReport, contentDescription = null, tint = Color(0xFFFF9500))
Text("debug console", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.debug_debug_console), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
Spacer(Modifier.weight(1f))
Text("clear", color = Color(0xFFBF1A1A), fontFamily = FontFamily.Monospace, modifier = Modifier.clickable {
Text(stringResource(R.string.debug_clear), color = Color(0xFFBF1A1A), fontFamily = FontFamily.Monospace, modifier = Modifier.clickable {
manager.clearDebugMessages()
})
}
@@ -19,6 +19,8 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.model.BitchatMessage
import androidx.compose.material3.ColorScheme
@@ -91,7 +93,7 @@ fun AudioMessageItem(
.clickable { onCancelTransfer?.invoke(message) },
contentAlignment = Alignment.Center
) {
Icon(imageVector = Icons.Filled.Close, contentDescription = "Cancel", tint = Color.White, modifier = Modifier.size(16.dp))
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(R.string.cd_cancel), tint = Color.White, modifier = Modifier.size(16.dp))
}
}
}
@@ -19,6 +19,8 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -28,6 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.bitchat.android.features.file.FileUtils
import com.bitchat.android.model.BitchatFilePacket
@@ -60,7 +63,7 @@ fun FileMessageItem(
// File icon
Icon(
imageVector = Icons.Filled.Description,
contentDescription = "File",
contentDescription = stringResource(com.bitchat.android.R.string.cd_file),
tint = getFileIconColor(packet.fileName),
modifier = Modifier.size(32.dp)
)
@@ -70,13 +73,13 @@ fun FileMessageItem(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
// File name
Text(
text = packet.fileName,
style = MaterialTheme.typography.bodyLarge,
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = packet.fileName,
style = MaterialTheme.typography.bodyLarge,
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// File details
Row(
@@ -14,6 +14,8 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import com.bitchat.android.features.file.FileUtils
@Composable
@@ -44,7 +46,7 @@ fun FilePickerButton(
) {
Icon(
imageVector = Icons.Filled.Attachment,
contentDescription = "Pick file",
contentDescription = stringResource(R.string.cd_pick_file),
tint = Color.Gray,
modifier = Modifier.size(20.dp).rotate(90f)
)
@@ -28,6 +28,9 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import kotlinx.coroutines.delay
/**
@@ -77,7 +80,7 @@ fun FileSendingAnimation(
// File icon
Icon(
imageVector = Icons.Filled.Description,
contentDescription = "File",
contentDescription = stringResource(R.string.cd_file),
tint = Color(0xFF00C851), // Green like app theme
modifier = Modifier.size(32.dp)
)
@@ -102,7 +105,7 @@ fun FileSendingAnimation(
// Blinking cursor (only if not fully revealed)
if (animatedChars < fileName.length && showCursor) {
androidx.compose.material3.Text(
text = "_",
text = stringResource(R.string.underscore),
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
color = Color.White
@@ -132,10 +135,12 @@ private fun FileProgressBars(
val filledBars = (progress * bars).toInt()
// Create a matrix-style progress bar string
val ctx = LocalContext.current
val progressString = buildString {
val brackets = ctx.getString(R.string.progress_bar_brackets, "", 0)
append("[")
for (i in 0 until bars) {
append(if (i < filledBars) "" else "")
append(if (i < filledBars) ctx.getString(R.string.progress_filled) else ctx.getString(R.string.progress_empty))
}
append("] ")
append("${(progress * 100).toInt()}%")
@@ -20,9 +20,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.bitchat.android.R
import com.bitchat.android.features.file.FileUtils
import com.bitchat.android.model.BitchatFilePacket
import kotlinx.coroutines.launch
@@ -54,7 +56,7 @@ fun FileViewerDialog(
) {
// File received header
Text(
text = "📎 File Received",
text = stringResource(R.string.file_viewer_title),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
@@ -65,17 +67,17 @@ fun FileViewerDialog(
horizontalAlignment = Alignment.Start
) {
Text(
text = "📄 ${packet.fileName}",
text = stringResource(R.string.file_viewer_name, packet.fileName),
style = MaterialTheme.typography.bodyLarge,
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium
)
Text(
text = "📏 Size: ${FileUtils.formatFileSize(packet.fileSize)}",
text = stringResource(R.string.file_viewer_size, FileUtils.formatFileSize(packet.fileSize)),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "🏷️ Type: ${packet.mimeType}",
text = stringResource(R.string.file_viewer_type, packet.mimeType),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -108,7 +110,7 @@ fun FileViewerDialog(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text("📂 Open / Save")
Text(stringResource(R.string.file_viewer_open_save))
}
// Dismiss button
@@ -119,13 +121,13 @@ fun FileViewerDialog(
containerColor = MaterialTheme.colorScheme.secondary
)
) {
Text("❌ Close")
Text(stringResource(R.string.close_with_emoji))
}
}
}
}
}
}
}
/**
* Attempts to open a file using system viewers or save to device
@@ -32,6 +32,8 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import java.io.File
/**
@@ -75,13 +77,13 @@ fun FullScreenImageViewer(imagePaths: List<String>, initialIndex: Int = 0, onClo
bmp?.let {
androidx.compose.foundation.Image(
bitmap = it.asImageBitmap(),
contentDescription = "Image ${page + 1} of ${imagePaths.size}",
contentDescription = stringResource(R.string.cd_image_index_of, page + 1, imagePaths.size),
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Fit
)
} ?: run {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = "Image unavailable", color = Color.White)
Text(text = stringResource(R.string.image_unavailable), color = Color.White)
}
}
}
@@ -96,7 +98,7 @@ fun FullScreenImageViewer(imagePaths: List<String>, initialIndex: Int = 0, onClo
.padding(horizontal = 12.dp, vertical = 4.dp)
) {
Text(
text = "${(pagerState.currentPage ?: 0) + 1} / ${imagePaths.size}",
text = stringResource(R.string.image_counter, (pagerState.currentPage ?: 0) + 1, imagePaths.size),
color = Color.White,
fontSize = 14.sp,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
@@ -118,7 +120,7 @@ fun FullScreenImageViewer(imagePaths: List<String>, initialIndex: Int = 0, onClo
.clickable { saveToDownloads(context, imagePaths[pagerState.currentPage].toString()) },
contentAlignment = Alignment.Center
) {
androidx.compose.material3.Icon(Icons.Filled.Download, "Save current image", tint = Color.White)
androidx.compose.material3.Icon(Icons.Filled.Download, stringResource(R.string.cd_save_current_image), tint = Color.White)
}
Spacer(Modifier.width(12.dp))
Box(
@@ -128,7 +130,7 @@ fun FullScreenImageViewer(imagePaths: List<String>, initialIndex: Int = 0, onClo
.clickable { onClose() },
contentAlignment = Alignment.Center
) {
androidx.compose.material3.Icon(Icons.Filled.Close, "Close", tint = Color.White)
androidx.compose.material3.Icon(Icons.Filled.Close, stringResource(R.string.cd_close), tint = Color.White)
}
}
}
@@ -161,10 +163,10 @@ private fun saveToDownloads(context: android.content.Context, path: String) {
context.contentResolver.update(uri, v2, null, null)
}
// Show toast message indicating the image has been saved
Toast.makeText(context, "Image saved to Downloads", Toast.LENGTH_SHORT).show()
Toast.makeText(context, context.getString(R.string.toast_image_saved), Toast.LENGTH_SHORT).show()
}
}.onFailure {
// Optionally handle failure case (e.g., show error toast)
Toast.makeText(context, "Failed to save image", Toast.LENGTH_SHORT).show()
Toast.makeText(context, context.getString(R.string.toast_failed_to_save_image), Toast.LENGTH_SHORT).show()
}
}
@@ -16,15 +16,17 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.text.font.FontFamily
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.BitchatMessageType
@@ -113,7 +115,7 @@ fun ImageMessageItem(
// Fully revealed image
Image(
bitmap = img,
contentDescription = "Image",
contentDescription = stringResource(com.bitchat.android.R.string.cd_image),
modifier = Modifier
.widthIn(max = 300.dp)
.aspectRatio(aspect)
@@ -137,13 +139,13 @@ fun ImageMessageItem(
.clickable { onCancelTransfer?.invoke(message) },
contentAlignment = Alignment.Center
) {
Icon(imageVector = Icons.Filled.Close, contentDescription = "Cancel", tint = Color.White, modifier = Modifier.size(14.dp))
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(com.bitchat.android.R.string.cd_cancel), tint = Color.White, modifier = Modifier.size(14.dp))
}
}
}
}
} else {
Text(text = "[image unavailable]", fontFamily = FontFamily.Monospace, color = Color.Gray)
Text(text = stringResource(com.bitchat.android.R.string.image_unavailable), fontFamily = FontFamily.Monospace, color = Color.Gray)
}
}
}
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitchat.android.features.media.ImageUtils
@@ -35,7 +36,7 @@ fun ImagePickerButton(
) {
Icon(
imageVector = Icons.Filled.Photo,
contentDescription = "Pick image",
contentDescription = stringResource(com.bitchat.android.R.string.pick_image),
tint = Color.Gray,
modifier = Modifier.size(20.dp)
)
@@ -26,6 +26,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
/**
* Media picker that offers image and file options
@@ -53,7 +55,7 @@ fun MediaPickerOptions(
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Pick media",
contentDescription = stringResource(R.string.cd_pick_media),
tint = Color.Black,
modifier = Modifier.size(20.dp)
)
@@ -98,7 +100,7 @@ fun MediaPickerOptions(
modifier = Modifier.size(16.dp)
)
androidx.compose.material3.Text(
text = "Image",
text = stringResource(R.string.media_type_image),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
@@ -126,7 +128,7 @@ fun MediaPickerOptions(
modifier = Modifier.size(16.dp)
)
androidx.compose.material3.Text(
text = "File",
text = stringResource(R.string.media_type_file),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)