This commit is contained in:
callebtc 2026-07-27 02:51:19 +02:00
parent ea0660e717
commit 15d56715b9
66 changed files with 476 additions and 662 deletions

View File

@ -14,6 +14,7 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.bitchat.android.core.ui.icon.BitChatIcon
import kotlinx.coroutines.Job
@ -30,6 +31,7 @@ fun BitChatBrandButton(
contentDescription: String,
modifier: Modifier = Modifier,
tint: Color = MaterialTheme.colorScheme.primary,
iconSize: Dp = 22.dp,
) {
var tapCount by remember { mutableIntStateOf(0) }
var resetJob by remember { mutableStateOf<Job?>(null) }
@ -63,9 +65,7 @@ fun BitChatBrandButton(
imageVector = BitChatIcon,
contentDescription = contentDescription,
tint = tint,
// 20.dp rather than 16.dp: the pixel-art bubble has heavy internal negative space,
// so it reads noticeably smaller than a solid glyph at the same nominal size.
modifier = Modifier.size(20.dp),
modifier = Modifier.size(iconSize),
)
}
}

View File

@ -1,34 +1,37 @@
package com.bitchat.android.core.ui.component.button
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitchat.android.R
import com.bitchat.android.ui.theme.LocalBitchatPalette
@Composable
fun CloseButton(
onClick: () -> Unit,
modifier: Modifier = Modifier.Companion
modifier: Modifier = Modifier
) {
val palette = LocalBitchatPalette.current
IconButton(
onClick = onClick,
modifier = modifier
.size(32.dp),
// 44.dp to match every other tap target in the app's chrome.
modifier = modifier.size(44.dp),
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
containerColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.1f)
contentColor = palette.textSecondary,
containerColor = palette.surfaceVariant
)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close",
modifier = Modifier.Companion.size(18.dp)
contentDescription = stringResource(R.string.close_plain),
modifier = Modifier.size(18.dp)
)
}
}
}

View File

@ -1,5 +1,8 @@
package com.bitchat.android.onboarding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Security
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -13,9 +16,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme

View File

@ -1,13 +1,13 @@
package com.bitchat.android.onboarding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment

View File

@ -1,9 +1,9 @@
package com.bitchat.android.onboarding
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment

View File

@ -1,10 +1,10 @@
package com.bitchat.android.onboarding
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment

View File

@ -1,9 +1,5 @@
package com.bitchat.android.onboarding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.LocationOn
@ -13,6 +9,10 @@ import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.Settings
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment

View File

@ -1,5 +1,17 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.filled.Terminal
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.WifiOff
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.animateColorAsState
@ -19,18 +31,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.filled.Terminal
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.WifiOff
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -70,10 +70,12 @@ internal val AboutHorizontalPadding = 20.dp
/** Card corner radius for grouped rows. */
internal val AboutCardShape = RoundedCornerShape(16.dp)
/** Two top-level views of the sheet. */
/**
* Two top-level views of the sheet: what the app is and how to drive it, versus the knobs.
*/
enum class AboutTab {
HowToUse,
BasicInfo,
Info,
Settings,
}
/**
@ -175,7 +177,7 @@ internal fun AboutTabBar(
var rowWidth by remember { mutableStateOf(0.dp) }
val tabWidth = rowWidth / 2
val indicatorOffset by animateDpAsState(
targetValue = if (selected == AboutTab.HowToUse) 0.dp else tabWidth,
targetValue = if (selected == AboutTab.Info) 0.dp else tabWidth,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "aboutTabIndicator"
)
@ -190,15 +192,15 @@ internal fun AboutTabBar(
) {
Row(modifier = Modifier.fillMaxWidth()) {
AboutTabLabel(
text = stringResource(R.string.about_tab_how_to_use),
isSelected = selected == AboutTab.HowToUse,
onClick = { onSelect(AboutTab.HowToUse) },
text = stringResource(R.string.about_tab_info),
isSelected = selected == AboutTab.Info,
onClick = { onSelect(AboutTab.Info) },
modifier = Modifier.weight(1f)
)
AboutTabLabel(
text = stringResource(R.string.about_tab_basic_info),
isSelected = selected == AboutTab.BasicInfo,
onClick = { onSelect(AboutTab.BasicInfo) },
text = stringResource(R.string.about_tab_settings),
isSelected = selected == AboutTab.Settings,
onClick = { onSelect(AboutTab.Settings) },
modifier = Modifier.weight(1f)
)
}
@ -272,8 +274,8 @@ private fun AboutInstructionRow(
contentDescription = null,
tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(18.dp)
.padding(top = 1.dp)
.size(22.dp)
)
Text(
text = text,
@ -315,7 +317,7 @@ internal fun AboutHowToUseSection(modifier: Modifier = Modifier) {
text = stringResource(R.string.about_howto_channels)
)
AboutInstructionRow(
icon = Icons.Filled.Groups,
icon = Icons.Filled.Person,
text = stringResource(R.string.about_howto_people)
)
AboutInstructionRow(
@ -428,7 +430,7 @@ private fun AboutFeatureRow(
tint = colorScheme.primary,
modifier = Modifier
.padding(top = 1.dp)
.size(22.dp)
.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
@ -580,7 +582,7 @@ internal fun SheetHeaderBadge(
imageVector = icon,
contentDescription = null,
tint = colorScheme.primary,
modifier = Modifier.size(24.dp)
modifier = Modifier.size(26.dp)
)
}
}

View File

@ -1,5 +1,12 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Speed
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
@ -10,13 +17,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -246,7 +246,7 @@ fun AboutSheet(
val colorScheme = MaterialTheme.colorScheme
val palette = LocalBitchatPalette.current
var selectedTab by remember { mutableStateOf(AboutTab.HowToUse) }
var selectedTab by remember { mutableStateOf(AboutTab.Info) }
if (isPresented) {
BitchatBottomSheet(
@ -273,21 +273,22 @@ fun AboutSheet(
)
}
if (selectedTab == AboutTab.HowToUse) {
if (selectedTab == AboutTab.Info) {
// What the app is, then how to drive it. Both are reference material a
// new user reads once, so they belong on the same tab.
item(key = "features") {
Column {
AboutSectionLabel(text = stringResource(R.string.about_section_about))
AboutFeatureCard()
}
}
item(key = "how_to_use") {
AboutHowToUseSection()
}
}
if (selectedTab == AboutTab.BasicInfo) {
// Features Section - Grouped Card
item(key = "features") {
Column {
AboutSectionLabel(text = stringResource(R.string.about_section_about))
AboutFeatureCard()
}
}
if (selectedTab == AboutTab.Settings) {
// Appearance Section
item(key = "appearance") {
Column {
@ -569,7 +570,7 @@ fun AboutSheet(
item(key = "warning") {
AboutWarningCard(modifier = Modifier.padding(top = 24.dp))
}
} // end BasicInfo tab
} // end Settings tab
// Footer
item(key = "footer") {

View File

@ -0,0 +1,112 @@
package com.bitchat.android.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.TextUnit
import com.bitchat.android.ui.theme.BitchatMotion
/**
* Counts that roll to their new value instead of snapping.
*
* Peer counts change on their own, without the user doing anything, so a hard digit swap is easy
* to miss and looks like a rendering fault when it is noticed. Sliding the digits in the
* direction the number moved up when someone joins, down when someone leaves conveys the
* change without needing a separate indicator.
*/
@Composable
fun AnimatedCount(
count: Int,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
prefix: String = "",
) {
AnimatedContent(
targetState = count,
transitionSpec = {
val goingUp = targetState > initialState
(
slideInVertically(
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
initialOffsetY = { height -> if (goingUp) height else -height }
) + fadeIn(tween(BitchatMotion.STANDARD_MS))
).togetherWith(
slideOutVertically(
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
targetOffsetY = { height -> if (goingUp) -height else height }
) + fadeOut(tween(BitchatMotion.QUICK_MS))
// Clip so the outgoing digit cannot bleed past the text bounds mid-transition.
) using SizeTransform(clip = true)
},
modifier = modifier,
label = "animatedCount"
) { value ->
Text(
text = "$prefix$value",
style = style,
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
fontFamily = fontFamily,
maxLines = 1
)
}
}
/**
* Cross-fades a label whose text embeds a count, e.g. `People (7)` or `3 people`.
*
* Used where the number is not isolated in its own composable and cannot be rolled on its own.
* The transition is keyed on [count] rather than on [text] so that a label changing for some
* other reason (a locale switch, say) does not animate.
*/
@Composable
fun AnimatedCountLabel(
count: Int,
text: String,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
) {
AnimatedContent(
targetState = count,
transitionSpec = {
fadeIn(tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing))
.togetherWith(fadeOut(tween(BitchatMotion.QUICK_MS)))
.using(SizeTransform(clip = false))
},
modifier = modifier,
label = "animatedCountLabel"
) { _ ->
Text(
text = text,
style = style,
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
fontFamily = fontFamily,
maxLines = 1
)
}
}

View File

@ -1,10 +1,20 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
@ -14,10 +24,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -45,14 +51,29 @@ import com.bitchat.android.ui.theme.LocalBitchatPalette
* Extracted from ChatScreen.kt for better organization
*/
/** Height of the chat top bar. Taller than the old 42.dp so 40.dp tap targets fit properly. */
val ChatHeaderHeight = 48.dp
/** Height of the chat top bar. Taller than the old 42.dp so 44.dp tap targets fit properly. */
val ChatHeaderHeight = 52.dp
/** Standard icon size in the header. */
private val HeaderIconSize = 18.dp
/**
* The single icon size used by every glyph in the top bar.
*
* Previously the bar mixed 12/14/16/18.dp icons, which is why it read as a collection of
* unrelated glyphs rather than one control strip. One size for everything, and large enough to
* actually see.
*/
internal val HeaderIconSize = 22.dp
/**
* Text size for the top bar's labels: nickname, channel name, peer count.
*
* A step up from the 15.sp body scale. The bar is the app's primary status readout and was
* noticeably harder to read than the messages below it; the extra point costs nothing because
* the bar's height is driven by [HeaderTapTarget], not by the text.
*/
private val HeaderTextSize = 17.sp
/** Minimum tap target for every interactive element in the header. */
private val HeaderTapTarget = 40.dp
private val HeaderTapTarget = 44.dp
/** Corner radius for the header's tappable label+icon clusters. */
private val HeaderClusterShape = RoundedCornerShape(8.dp)
@ -81,6 +102,14 @@ private fun HeaderIconButton(
}
}
/**
* Connection status indicator.
*
* Only visible while the connection is still coming up (or has failed). A steady green "all
* good" light is noise: the absence of the indicator already means everything is fine, and the
* eye stops registering an always-on dot anyway. It fades and scales in/out so appearing and
* disappearing reads as a state change rather than a glitch.
*/
@Composable
fun TorStatusDot(
modifier: Modifier = Modifier
@ -88,27 +117,35 @@ fun TorStatusDot(
val palette = LocalBitchatPalette.current
val torProvider = remember { com.bitchat.android.net.ArtiTorManager.getInstance() }
val torStatus by torProvider.statusFlow.collectAsState()
if (torStatus.mode != com.bitchat.android.net.TorMode.OFF) {
val targetColor = when {
torStatus.running && torStatus.bootstrapPercent < 100 -> palette.accentOrange
torStatus.running && torStatus.bootstrapPercent >= 100 -> palette.accentGreen
else -> palette.accentRed
}
// Cross-fade rather than snap: Tor flips through bootstrapping states frequently and a
// hard colour cut in the corner of the screen reads as a glitch.
val isEnabled = torStatus.mode != com.bitchat.android.net.TorMode.OFF
val isEstablished = torStatus.running && torStatus.bootstrapPercent >= 100
val isVisible = isEnabled && !isEstablished
AnimatedVisibility(
visible = isVisible,
enter = fadeIn(tween(BitchatMotion.STANDARD_MS)) +
scaleIn(
initialScale = 0.4f,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing)
),
exit = fadeOut(tween(BitchatMotion.QUICK_MS)) +
scaleOut(
targetScale = 0.4f,
animationSpec = tween(BitchatMotion.QUICK_MS, easing = FastOutSlowInEasing)
),
) {
// Bootstrapping vs. failed. Cross-faded, because Tor flips between these frequently and
// a hard colour cut in the corner of the screen reads as a rendering fault.
val dotColor by animateColorAsState(
targetValue = targetColor,
targetValue = if (torStatus.running) palette.accentOrange else palette.accentRed,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "torDotColor"
)
Canvas(
modifier = modifier
) {
val radius = size.minDimension / 2
Canvas(modifier = modifier) {
drawCircle(
color = dotColor,
radius = radius,
radius = size.minDimension / 2,
center = Offset(size.width / 2, size.height / 2)
)
}
@ -179,15 +216,17 @@ fun NicknameEditor(
Text(
text = stringResource(R.string.at_symbol),
style = MaterialTheme.typography.bodyMedium,
fontSize = HeaderTextSize,
color = colorScheme.primary.copy(alpha = 0.7f)
)
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = MaterialTheme.typography.bodyMedium.copy(
color = colorScheme.primary,
fontFamily = FontFamily.Monospace
fontFamily = FontFamily.Monospace,
fontSize = HeaderTextSize
),
cursorBrush = SolidColor(colorScheme.primary),
singleLine = true,
@ -198,7 +237,7 @@ fun NicknameEditor(
}
),
modifier = Modifier
.widthIn(max = 140.dp)
.widthIn(max = 150.dp)
.horizontalScroll(scrollState)
)
}
@ -250,9 +289,9 @@ fun PeerCounter(
.padding(horizontal = 6.dp)
) {
Icon(
// `Groups` reads as a crowd at 18.dp, where `Group`'s two-person silhouette turns
// into an indistinct blob.
imageVector = Icons.Filled.Groups,
// A single silhouette rather than a crowd: at 22.dp a multi-person glyph collapses
// into an indistinct blob, and the number beside it already conveys "how many".
imageVector = Icons.Filled.Person,
contentDescription = when (selectedLocationChannel) {
is com.bitchat.android.geohash.ChannelID.Location -> stringResource(R.string.cd_geohash_participants)
else -> stringResource(R.string.cd_connected_peers)
@ -261,17 +300,20 @@ fun PeerCounter(
tint = animatedCountColor
)
Text(
text = "$peopleCount",
AnimatedCount(
count = peopleCount,
style = MaterialTheme.typography.bodyMedium,
fontSize = HeaderTextSize,
color = animatedCountColor,
fontWeight = FontWeight.Medium
)
if (joinedChannels.isNotEmpty()) {
Text(
text = stringResource(R.string.channel_count_prefix) + "${joinedChannels.size}",
AnimatedCount(
count = joinedChannels.size,
prefix = stringResource(R.string.channel_count_prefix),
style = MaterialTheme.typography.bodyMedium,
fontSize = HeaderTextSize,
color = if (isConnected) palette.accentGreen else palette.accentRed,
fontWeight = FontWeight.Medium
)
@ -343,7 +385,7 @@ private fun ChannelHeader(
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
modifier = Modifier.size(20.dp),
modifier = Modifier.size(HeaderIconSize),
tint = colorScheme.primary
)
}
@ -352,6 +394,7 @@ private fun ChannelHeader(
Text(
text = "#$channel",
style = MaterialTheme.typography.titleMedium,
fontSize = HeaderTextSize,
color = colorScheme.primary,
modifier = Modifier
.align(Alignment.Center)
@ -368,6 +411,7 @@ private fun ChannelHeader(
Text(
text = stringResource(R.string.chat_leave),
style = MaterialTheme.typography.labelMedium,
fontSize = 15.sp,
color = palette.accentRed
)
}
@ -395,11 +439,6 @@ private fun MainHeader(
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
// Bookmarks store for current geohash toggle (iOS parity)
val context = androidx.compose.ui.platform.LocalContext.current
val bookmarksStore = remember { com.bitchat.android.geohash.GeohashBookmarksStore.getInstance(context) }
val bookmarks by bookmarksStore.bookmarks.collectAsStateWithLifecycle()
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
@ -423,6 +462,7 @@ private fun MainHeader(
Text(
text = "/",
style = MaterialTheme.typography.bodyMedium,
fontSize = HeaderTextSize,
// Dimmed: the slash is a separator, not content. At full brightness it competed
// with the nickname beside it.
color = colorScheme.primary.copy(alpha = 0.45f),
@ -435,12 +475,26 @@ private fun MainHeader(
)
}
// MARK: - Status cluster. Order matches the design: bookmark, channel, people.
// MARK: - Status cluster.
//
// Order, left to right: connection state, unread DMs, notes, channel, people.
// The connection indicator leads because it is the only item that can invalidate
// everything to its right, and because it appears and disappears on its own — a fixed
// leftmost slot means the rest of the cluster never shifts when it does.
Row(
verticalAlignment = Alignment.CenterVertically,
// Tight, because every child below is its own >=40.dp tap target.
// Tight, because every child below is its own >=44.dp tap target.
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
// Connection status. The Box holds the slot open at a constant width so the icons to
// its right stay put whether or not the dot is currently showing.
Box(
modifier = Modifier.size(HeaderIconSize),
contentAlignment = Alignment.Center
) {
TorStatusDot(modifier = Modifier.size(8.dp))
}
// Unread private messages badge (click to open most recent DM)
if (hasUnreadPrivateMessages.isNotEmpty()) {
HeaderIconButton(
@ -462,37 +516,9 @@ private fun MainHeader(
onClick = onLocationNotesClick
)
// Tor status dot. Wrapped in a fixed box so its optical position no longer depends
// on ad-hoc asymmetric padding.
Box(modifier = Modifier.size(16.dp), contentAlignment = Alignment.Center) {
TorStatusDot(modifier = Modifier.size(6.dp))
}
// PoW status indicator
PoWStatusIndicator(
modifier = Modifier,
style = PoWIndicatorStyle.COMPACT
)
// Bookmark toggle for current geohash (not shown for mesh)
val currentGeohash: String? = when (val sc = selectedLocationChannel) {
is com.bitchat.android.geohash.ChannelID.Location -> sc.channel.geohash
else -> null
}
if (currentGeohash != null) {
val isBookmarked = bookmarks.contains(currentGeohash)
HeaderIconButton(
onClick = { bookmarksStore.toggle(currentGeohash) },
contentDescription = stringResource(R.string.cd_toggle_bookmark)
) {
Icon(
imageVector = if (isBookmarked) Icons.Filled.Bookmark else Icons.Outlined.BookmarkBorder,
contentDescription = stringResource(R.string.cd_toggle_bookmark),
tint = if (isBookmarked) palette.accentGreen else palette.textSecondary,
modifier = Modifier.size(HeaderIconSize)
)
}
}
// Bookmarking lives in the Location Channels sheet, one tap away via the channel
// button below. Duplicating it here bought a shortcut for a rare action at the cost
// of a slot in the app's most crowded row.
LocationChannelsButton(
viewModel = viewModel,
@ -532,8 +558,11 @@ private fun LocationChannelsButton(
val isLocation = selectedChannel is com.bitchat.android.geohash.ChannelID.Location
val badgeText = when (val channel = selectedChannel) {
// Geohashes keep the '#' because that is how they are written and typed everywhere else.
is com.bitchat.android.geohash.ChannelID.Location -> "#${channel.channel.geohash}"
else -> "#mesh"
// The local mesh is not a hashtag channel, and the Bluetooth glyph already says what it
// is, so it is plain "mesh".
else -> stringResource(R.string.mesh_label)
}
val badgeColor = if (isLocation) palette.accentGreen else palette.accentBlue
val badgeIcon = if (isLocation) Icons.Outlined.Public else Icons.Filled.Bluetooth
@ -557,6 +586,7 @@ private fun LocationChannelsButton(
Text(
text = badgeText,
style = MaterialTheme.typography.bodyMedium,
fontSize = HeaderTextSize,
fontWeight = FontWeight.Medium,
color = badgeColor,
maxLines = 1
@ -567,7 +597,7 @@ private fun LocationChannelsButton(
Icon(
imageVector = Icons.Default.PinDrop,
contentDescription = stringResource(R.string.cd_teleported),
modifier = Modifier.size(12.dp),
modifier = Modifier.size(HeaderIconSize),
tint = badgeColor
)
}

View File

@ -4,6 +4,8 @@ package com.bitchat.android.ui
// [Goose] Installing FileShareDispatcher handler in ChatScreen to forward file sends to ViewModel
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.animation.*
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
@ -14,8 +16,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Alignment
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.IconButton
@ -303,6 +303,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
Icon(
imageVector = Icons.Filled.ArrowDownward,
contentDescription = stringResource(com.bitchat.android.R.string.cd_scroll_to_bottom),
modifier = Modifier.size(22.dp),
tint = palette.accentGreen
)
}

View File

@ -1,13 +1,13 @@
package com.bitchat.android.ui
import android.util.Log
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Explore
import androidx.compose.material.icons.outlined.LocationOn
import android.util.Log
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -193,7 +193,7 @@ private fun GeohashPersonItem(
Icon(
imageVector = Icons.Filled.Email,
contentDescription = stringResource(R.string.cd_unread_message),
modifier = Modifier.size(16.dp),
modifier = Modifier.size(18.dp),
tint = palette.accentOrange
)
} else {
@ -205,7 +205,7 @@ private fun GeohashPersonItem(
else -> "face.smiling" to palette.textSecondary
}
// Use appropriate Material icon (closest match to iOS SF Symbols)
// Teleported users get the pin (they chose this location); locals get the person.
val icon = when (iconName) {
"face.dashed" -> Icons.Outlined.Explore
else -> Icons.Outlined.LocationOn
@ -214,8 +214,8 @@ private fun GeohashPersonItem(
Icon(
imageVector = icon,
contentDescription = if (isTeleported || isMyTeleported) "Teleported user" else "User",
modifier = Modifier.size(16.dp),
tint = iconColor.copy(alpha = if (iconName == "face.dashed") 0.6f else 1.0f) // Make dashed faces slightly transparent
modifier = Modifier.size(18.dp),
tint = iconColor.copy(alpha = if (iconName == "face.dashed") 0.6f else 1.0f)
)
}

View File

@ -1,5 +1,9 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Remove
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
@ -15,10 +19,6 @@ import android.webkit.WebViewClient
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api

View File

@ -210,22 +210,11 @@ class GeohashViewModel(
powDifficulty = if (pow.enabled) pow.difficulty else null
)
messageManager.addChannelMessage("geo:${channel.geohash}", localMsg)
val startedMining = pow.enabled && pow.difficulty > 0
if (startedMining) {
com.bitchat.android.ui.PoWMiningTracker.startMiningMessage(tempId)
}
try {
val identity = NostrIdentityBridge.deriveIdentity(forGeohash = channel.geohash, context = getApplication())
val teleported = state.isTeleported.value
val event = NostrProtocol.createEphemeralGeohashEvent(content, channel.geohash, identity, nickname, teleported)
val relayManager = NostrRelayManager.getInstance(getApplication())
relayManager.sendEventToGeohash(event, channel.geohash, includeDefaults = false, nRelays = 5)
} finally {
// Ensure we stop the per-message mining animation regardless of success/failure
if (startedMining) {
com.bitchat.android.ui.PoWMiningTracker.stopMiningMessage(tempId)
}
}
val identity = NostrIdentityBridge.deriveIdentity(forGeohash = channel.geohash, context = getApplication())
val teleported = state.isTeleported.value
val event = NostrProtocol.createEphemeralGeohashEvent(content, channel.geohash, identity, nickname, teleported)
val relayManager = NostrRelayManager.getInstance(getApplication())
relayManager.sendEventToGeohash(event, channel.geohash, includeDefaults = false, nRelays = 5)
} catch (e: Exception) {
Log.e(TAG, "Failed to send geohash message: ${e.message}")
}

View File

@ -2,6 +2,8 @@ package com.bitchat.android.ui
// [Goose] TODO: Replace inline file attachment stub with FilePickerButton abstraction that dispatches via FileShareDispatcher
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
@ -12,8 +14,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -187,10 +187,10 @@ private val ComposerMinHeight = 44.dp
private val ComposerShape = RoundedCornerShape(22.dp)
/** Diameter of the circular send affordance nested inside the pill. */
private val SendButtonSize = 32.dp
private val SendButtonSize = 36.dp
/** Tap target for the media affordances sitting outside the pill. */
private val MediaButtonSize = 40.dp
/** Icon size shared by the composer's glyphs, matching the top bar. */
internal val ComposerIconSize = 22.dp
@Composable
fun MessageInput(
@ -325,7 +325,7 @@ fun MessageInput(
hasText = hasText,
isAccented = selectedPrivatePeer != null || currentChannel != null,
onSend = onSend,
modifier = Modifier.padding(end = 6.dp, bottom = 6.dp)
modifier = Modifier.padding(end = 4.dp, bottom = 4.dp)
)
}
@ -430,7 +430,7 @@ private fun SendButton(
Icon(
imageVector = Icons.Filled.ArrowUpward,
contentDescription = stringResource(id = R.string.send_message),
modifier = Modifier.size(18.dp),
modifier = Modifier.size(ComposerIconSize),
tint = tint
)
}

View File

@ -1,5 +1,7 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Link
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.background
@ -7,8 +9,6 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Link
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment

View File

@ -1,5 +1,12 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Map
import androidx.compose.material.icons.filled.PinDrop
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material.icons.outlined.Public
import android.content.Intent
import android.net.Uri
import android.provider.Settings
@ -15,13 +22,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Map
import androidx.compose.material.icons.filled.PinDrop
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -218,6 +218,7 @@ fun LocationChannelsSheet(
title = meshTitleWithCount(viewModel),
subtitle = stringResource(R.string.location_bluetooth_subtitle, bluetoothRangeString()),
isSelected = selectedChannel is ChannelID.Mesh,
participantCount = meshCount(viewModel),
titleColor = standardBlue,
titleBold = meshCount(viewModel) > 0,
trailingContent = null,
@ -251,6 +252,7 @@ fun LocationChannelsSheet(
title = geohashTitleWithCount(channel, participantCount),
subtitle = subtitlePrefix + (namePart?.let { "$it" } ?: ""),
isSelected = isChannelSelected(channel, selectedChannel),
participantCount = participantCount,
titleColor = standardGreen,
titleBold = highlight,
trailingContent = {
@ -259,7 +261,7 @@ fun LocationChannelsSheet(
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 = if (isBookmarked) palette.accentGreen else palette.textSecondary,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(22.dp)
)
}
},
@ -314,6 +316,7 @@ fun LocationChannelsSheet(
title = title,
subtitle = subtitle,
isSelected = isChannelSelected(channel, selectedChannel),
participantCount = participantCount,
titleColor = null,
titleBold = participantCount > 0,
trailingContent = {
@ -322,7 +325,7 @@ fun LocationChannelsSheet(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.cd_remove_bookmark),
tint = palette.accentGreen,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(22.dp)
)
}
},
@ -432,7 +435,7 @@ fun LocationChannelsSheet(
imageVector = Icons.Filled.Map,
contentDescription = stringResource(R.string.cd_open_map),
tint = palette.textSecondary,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(22.dp)
)
}
@ -468,7 +471,7 @@ fun LocationChannelsSheet(
Icon(
imageVector = Icons.Filled.PinDrop,
contentDescription = stringResource(R.string.cd_teleport),
modifier = Modifier.size(12.dp),
modifier = Modifier.size(14.dp),
tint = teleportColor
)
Text(
@ -641,6 +644,8 @@ private fun ChannelRow(
title: String,
subtitle: String,
isSelected: Boolean,
/** Drives the count animation; the number itself is already baked into [title]. */
participantCount: Int,
titleColor: Color? = null,
titleBold: Boolean = false,
trailingContent: (@Composable (() -> Unit))? = null,
@ -701,7 +706,10 @@ private fun ChannelRow(
)
countSuffix?.let { count ->
Text(
AnimatedCountLabel(
// Participant counts are polled from the relays, so they change
// while the sheet is open.
count = participantCount,
text = count,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,

View File

@ -1,8 +1,8 @@
package com.bitchat.android.ui
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Description
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -55,12 +55,12 @@ fun LocationNotesButton(
val hasNotes = notesCount > 0
IconButton(
onClick = onClick,
modifier = modifier.size(40.dp)
modifier = modifier.size(44.dp)
) {
Icon(
imageVector = Icons.Outlined.Description, // "long.text.page.and.pencil" equivalent
imageVector = Icons.Outlined.Description,
contentDescription = stringResource(R.string.cd_location_notes),
modifier = Modifier.size(18.dp),
modifier = Modifier.size(HeaderIconSize),
tint = if (hasNotes) colorScheme.primary else LocalBitchatPalette.current.textSecondary
)
}

View File

@ -1,5 +1,7 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -9,8 +11,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue

View File

@ -1,183 +0,0 @@
package com.bitchat.android.ui
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatMessage
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import kotlin.random.Random
/**
* Animation state for individual characters
*/
private enum class CharacterAnimationState {
ENCRYPTED, // Showing random encrypted characters
FINAL // Showing final decrypted character
}
/**
* Check if a message should be animated based on its mining state
*/
@Composable
fun shouldAnimateMessage(messageId: String): Boolean {
val miningMessages by PoWMiningTracker.miningMessages.collectAsStateWithLifecycle()
return miningMessages.contains(messageId)
}
/**
* Tracks which messages are currently being mined for PoW
* Provides reactive state for UI animations
*/
object PoWMiningTracker {
private val _miningMessages = MutableStateFlow<Set<String>>(emptySet())
val miningMessages: StateFlow<Set<String>> = _miningMessages.asStateFlow()
/**
* Start tracking a message as mining
*/
fun startMiningMessage(messageId: String) {
_miningMessages.value = _miningMessages.value + messageId
}
/**
* Stop tracking a message as mining
*/
fun stopMiningMessage(messageId: String) {
_miningMessages.value = _miningMessages.value - messageId
}
/**
* Check if a message is currently mining
*/
fun isMiningMessage(messageId: String): Boolean {
return _miningMessages.value.contains(messageId)
}
/**
* Clear all mining messages (for cleanup)
*/
fun clearAllMining() {
_miningMessages.value = emptySet()
}
}
/**
* Shows the active PoW animation inside the same two-row layout used by static text messages.
*/
@Composable
fun MessageWithMatrixAnimation(
message: BitchatMessage,
currentUserNickname: String,
meshService: MeshService,
colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat,
onNicknameClick: ((String) -> Unit)?,
onMessageLongPress: ((BitchatMessage) -> Unit)?,
modifier: Modifier = Modifier,
showSender: Boolean = true,
) {
AnimatedMessageDisplay(
message = message,
currentUserNickname = currentUserNickname,
meshService = meshService,
colorScheme = colorScheme,
timeFormatter = timeFormatter,
onNicknameClick = onNicknameClick,
onMessageLongPress = onMessageLongPress,
modifier = modifier,
showSender = showSender,
)
}
/**
* Animates only the body content; sender, metadata, gestures, and spacing remain stable.
*/
@Composable
private fun AnimatedMessageDisplay(
message: BitchatMessage,
currentUserNickname: String,
meshService: MeshService,
colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat,
onNicknameClick: ((String) -> Unit)?,
onMessageLongPress: ((BitchatMessage) -> Unit)?,
modifier: Modifier = Modifier,
showSender: Boolean = true,
) {
var animatedContent by remember(message.id, message.content) {
mutableStateOf(message.content)
}
// Character-by-character animation state like the JavaScript version
var characterStates by remember(message.id, message.content) {
mutableStateOf(message.content.map { char ->
if (char == ' ') CharacterAnimationState.FINAL else CharacterAnimationState.ENCRYPTED
})
}
LaunchedEffect(message.id, message.content) {
if (message.content.isEmpty()) return@LaunchedEffect
val encryptedChars = "!@$%^&*()_+-=[]{}|;:,<>?".toCharArray()
// Start character animations with staggered delays (like JS version).
message.content.forEachIndexed { index, targetChar ->
if (targetChar != ' ') {
launch {
delay(index * 50L)
while (true) {
while (characterStates.getOrNull(index) == CharacterAnimationState.ENCRYPTED) {
val newContent = animatedContent.toCharArray()
if (index < newContent.size) {
newContent[index] = encryptedChars[Random.nextInt(encryptedChars.size)]
animatedContent = String(newContent)
}
delay(100L)
if (Random.nextFloat() < 0.1f) {
val finalContent = animatedContent.toCharArray()
if (index < finalContent.size) {
finalContent[index] = targetChar
animatedContent = String(finalContent)
}
val finalStates = characterStates.toMutableList()
finalStates[index] = CharacterAnimationState.FINAL
characterStates = finalStates
break
}
}
delay(2000L)
val resetStates = characterStates.toMutableList()
resetStates[index] = CharacterAnimationState.ENCRYPTED
characterStates = resetStates
}
}
}
}
}
TextMessageLayout(
message = message,
currentUserNickname = currentUserNickname,
meshService = meshService,
colorScheme = colorScheme,
timeFormatter = timeFormatter,
onNicknameClick = onNicknameClick,
onMessageLongPress = onMessageLongPress,
modifier = modifier,
showSender = showSender,
bodyContent = animatedContent,
)
}

View File

@ -1,19 +1,25 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import com.bitchat.android.R
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -37,6 +43,7 @@ import com.bitchat.android.favorites.FavoritesPersistenceService
import com.bitchat.android.geohash.ChannelID
import com.bitchat.android.identity.SecureIdentityStateManager
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import com.bitchat.android.ui.theme.BitchatMotion
import com.bitchat.android.ui.theme.LocalBitchatPalette
import com.bitchat.android.nostr.GeohashAliasRegistry
import com.bitchat.android.nostr.GeohashConversationRegistry
@ -116,8 +123,9 @@ fun MeshPeerListSheet(
.padding(horizontal = SheetHorizontalPadding + 14.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
SheetHeaderBadge(icon = Icons.Filled.Groups)
Text(
SheetHeaderBadge(icon = Icons.Filled.Person)
AnimatedCountLabel(
count = peopleCount,
text = stringResource(R.string.people_count_title, peopleCount),
fontSize = 20.sp,
fontFamily = FontFamily.Monospace,
@ -212,13 +220,13 @@ fun MeshPeerListSheet(
if (selectedLocationChannel !is ChannelID.Location) {
IconButton(
onClick = onShowVerification,
modifier = Modifier.size(24.dp)
modifier = Modifier.size(44.dp)
) {
Icon(
imageVector = Icons.Outlined.QrCode,
contentDescription = stringResource(R.string.verify_title),
tint = colorScheme.onSurface.copy(alpha = 0.8f),
modifier = Modifier.size(18.dp)
tint = LocalBitchatPalette.current.textSecondary,
modifier = Modifier.size(22.dp)
)
}
}
@ -231,6 +239,9 @@ fun MeshPeerListSheet(
}
}
/** Icon size for rows inside the sheets: one step down from the top bar. */
private val PeerRowIconSize = 18.dp
@Composable
private fun ChannelRow(
channel: String,
@ -580,28 +591,26 @@ private fun PeerItem(
) {
// Connection/status indicator
if (hasUnreadDM) {
// Show mail icon for unread DMs (iOS orange)
Icon(
imageVector = Icons.Filled.Email,
contentDescription = stringResource(R.string.cd_unread_message),
modifier = Modifier.size(16.dp),
tint = Color(0xFFFF9500) // iOS orange
modifier = Modifier.size(PeerRowIconSize),
tint = palette.accentOrange
)
} else if (showNostrGlobe) {
// Purple globe to indicate Nostr availability
Icon(
imageVector = Icons.Filled.Public,
contentDescription = stringResource(R.string.cd_reachable_via_nostr),
modifier = Modifier.size(16.dp),
tint = Color(0xFF9C27B0) // Purple
modifier = Modifier.size(PeerRowIconSize),
tint = palette.accentPurple
)
} else if (!isDirect && isFavorite) {
// Offline favorited user: show outlined circle icon
// Offline favourite: routed glyph, dimmed to read as unavailable.
Icon(
imageVector = Icons.Outlined.Circle,
contentDescription = stringResource(R.string.cd_offline_favorite),
modifier = Modifier.size(16.dp),
tint = Color.Gray
modifier = Modifier.size(PeerRowIconSize),
tint = palette.textTertiary
)
} else {
Icon(
@ -615,8 +624,8 @@ private fun PeerItem(
isDirect -> "Direct Bluetooth"
else -> "Routed"
},
modifier = Modifier.size(16.dp),
tint = colorScheme.onSurface.copy(alpha = 0.6f)
modifier = Modifier.size(PeerRowIconSize),
tint = palette.textSecondary
)
}
@ -649,8 +658,8 @@ private fun PeerItem(
Icon(
imageVector = Icons.Filled.Wifi,
contentDescription = "Direct Wi-Fi Aware",
modifier = Modifier.size(13.dp),
tint = colorScheme.onSurface.copy(alpha = 0.8f)
modifier = Modifier.size(PeerRowIconSize),
tint = palette.textSecondary
)
}
}
@ -660,9 +669,9 @@ private fun PeerItem(
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Filled.Verified,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = Color(0xFF32D74B) // iOS Green
contentDescription = stringResource(R.string.verify_title),
modifier = Modifier.size(PeerRowIconSize),
tint = palette.accentGreen
)
}
@ -673,13 +682,13 @@ private fun PeerItem(
// Favorite star with proper filled/outlined states
IconButton(
onClick = onToggleFavorite,
modifier = Modifier.size(32.dp)
modifier = Modifier.size(44.dp)
) {
Icon(
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
modifier = Modifier.size(16.dp),
tint = if (isFavorite) Color(0xFFFFD700) else Color(0xFF4CAF50)
modifier = Modifier.size(PeerRowIconSize),
tint = if (isFavorite) palette.accentOrange else palette.textTertiary
)
}
}
@ -696,25 +705,41 @@ private fun UnreadBadge(
colorScheme: ColorScheme,
modifier: Modifier = Modifier
) {
if (count > 0) {
val palette = LocalBitchatPalette.current
// Scale/fade in and out so a badge appearing while the sheet is open is noticed, and one
// clearing does not just blink away.
AnimatedVisibility(
visible = count > 0,
enter = fadeIn(tween(BitchatMotion.STANDARD_MS)) +
scaleIn(
initialScale = 0.5f,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing)
),
exit = fadeOut(tween(BitchatMotion.QUICK_MS)) +
scaleOut(
targetScale = 0.5f,
animationSpec = tween(BitchatMotion.QUICK_MS, easing = FastOutSlowInEasing)
),
modifier = modifier
) {
Box(
modifier = modifier
modifier = Modifier
.background(
color = Color(0xFFFFD700), // Yellow color
color = palette.accentOrange,
shape = RoundedCornerShape(10.dp)
)
.padding(horizontal = 6.dp, vertical = 2.dp)
.defaultMinSize(minWidth = 18.dp, minHeight = 18.dp),
contentAlignment = Alignment.Center
) {
Text(
AnimatedCountLabel(
count = count,
text = if (count > 99) "99+" else count.toString(),
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
),
color = Color.Black // Black text on yellow background
style = MaterialTheme.typography.labelSmall,
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
color = Color.Black
)
}
}

View File

@ -1,6 +1,8 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -17,8 +19,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -393,24 +393,7 @@ fun MessageItem(
return
}
// Check if this message should be animated during PoW mining
val shouldAnimate = shouldAnimateMessage(message.id)
// If animation is needed, use the matrix animation component for content only
if (shouldAnimate) {
// Display message with matrix animation for content
MessageWithMatrixAnimation(
message = message,
currentUserNickname = currentUserNickname,
meshService = meshService,
colorScheme = colorScheme,
timeFormatter = timeFormatter,
showSender = showSender,
onNicknameClick = onNicknameClick,
onMessageLongPress = onMessageLongPress,
modifier = modifier
)
} else if (message.sender == "system") {
if (message.sender == "system") {
// Background narration: `// Tor started. Routing all chats…`
val annotatedText = remember(message, palette) {
formatSystemMessage(

View File

@ -1,133 +0,0 @@
package com.bitchat.android.ui
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontFamily
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.R
import com.bitchat.android.nostr.PoWPreferenceManager
/**
* Shows the current Proof of Work status and settings
*/
@Composable
fun PoWStatusIndicator(
modifier: Modifier = Modifier,
style: PoWIndicatorStyle = PoWIndicatorStyle.COMPACT
) {
val powEnabled by PoWPreferenceManager.powEnabled.collectAsStateWithLifecycle()
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsStateWithLifecycle()
val isMining by PoWPreferenceManager.isMining.collectAsStateWithLifecycle()
val colorScheme = MaterialTheme.colorScheme
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
if (!powEnabled) return
when (style) {
PoWIndicatorStyle.COMPACT -> {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// PoW icon with animation if mining
if (isMining) {
val rotation by rememberInfiniteTransition(label = "pow-rotation").animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "pow-icon-rotation"
)
Icon(
imageVector = Icons.Filled.Security,
contentDescription = stringResource(R.string.cd_mining_pow),
tint = Color(0xFFFF9500), // Orange for mining
modifier = Modifier
.size(12.dp)
.graphicsLayer { rotationZ = rotation }
)
} else {
Icon(
imageVector = Icons.Filled.Security,
contentDescription = stringResource(R.string.cd_pow_enabled),
tint = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D), // Green when ready
modifier = Modifier.size(12.dp)
)
}
}
}
PoWIndicatorStyle.DETAILED -> {
Surface(
modifier = modifier,
color = colorScheme.surfaceVariant.copy(alpha = 0.3f),
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
// PoW icon
Icon(
imageVector = Icons.Filled.Security,
contentDescription = stringResource(R.string.cd_proof_of_work),
tint = if (isMining) Color(0xFFFF9500) else {
if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
},
modifier = Modifier.size(14.dp)
)
// Status text
Text(
text = if (isMining) {
stringResource(R.string.pow_mining_ellipsis)
} else {
stringResource(R.string.pow_label_format, powDifficulty)
},
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = if (isMining) Color(0xFFFF9500) else {
colorScheme.onSurface.copy(alpha = 0.7f)
}
)
// Time estimate
if (!isMining && powDifficulty > 0) {
Text(
text = stringResource(R.string.pow_time_estimate, NostrProofOfWork.estimateMiningTime(powDifficulty)),
fontSize = 9.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.5f)
)
}
}
}
}
}
}
/**
* Style options for the PoW status indicator
*/
enum class PoWIndicatorStyle {
COMPACT, // Small icon + difficulty number
DETAILED // Icon + status text + time estimate
}

View File

@ -1,5 +1,12 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.NoEncryption
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Warning as OutlinedWarning
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -12,13 +19,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.NoEncryption
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Warning as OutlinedWarning
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu

View File

@ -1,5 +1,7 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.QrCodeScanner
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
@ -31,8 +33,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.QrCodeScanner
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api

View File

@ -1,13 +1,13 @@
package com.bitchat.android.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import android.Manifest
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -132,7 +132,7 @@ fun VoiceRecordButton(
imageVector = Icons.Filled.Mic,
contentDescription = stringResource(com.bitchat.android.R.string.cd_record_voice),
tint = Color.Black,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(22.dp)
)
}
}

View File

@ -1,5 +1,13 @@
package com.bitchat.android.ui.debug
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiTethering
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.PowerSettingsNew
import androidx.compose.material.icons.filled.SettingsEthernet
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -9,14 +17,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiTethering
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.PowerSettingsNew
import androidx.compose.material.icons.filled.SettingsEthernet
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable

View File

@ -1,14 +1,14 @@
package com.bitchat.android.ui.debug
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.outlined.Bluetooth
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.outlined.Bluetooth
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier

View File

@ -1,11 +1,11 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment

View File

@ -1,5 +1,7 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Description
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -12,8 +14,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Description
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon

View File

@ -1,11 +1,11 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Attachment
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Attachment
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable

View File

@ -1,5 +1,7 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Description
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@ -11,8 +13,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Description
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

View File

@ -1,5 +1,8 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Download
import android.content.ContentValues
import android.os.Build
import android.provider.MediaStore
@ -10,9 +13,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Download
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface

View File

@ -1,12 +1,12 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*

View File

@ -1,5 +1,9 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.Photo
import androidx.compose.material.icons.filled.PhotoCamera
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
@ -10,10 +14,6 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Camera
import androidx.compose.material.icons.filled.Photo
import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -91,8 +91,8 @@ fun ImagePickerButton(
Box(
modifier = modifier
// 40.dp to match the header's tap targets and the composer's send button.
.size(40.dp)
// Matches the header's tap targets and the composer's send button.
.size(44.dp)
.clip(CircleShape)
.combinedClickable(
onClick = { imagePicker.launch("image/*") },
@ -110,7 +110,7 @@ fun ImagePickerButton(
imageVector = Icons.Filled.PhotoCamera,
contentDescription = stringResource(com.bitchat.android.R.string.pick_image),
tint = LocalBitchatPalette.current.textSecondary,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(22.dp)
)
}

View File

@ -1,5 +1,8 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Description
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -9,9 +12,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Description
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

View File

@ -1,13 +1,13 @@
package com.bitchat.android.ui.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import android.media.MediaPlayer
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme

View File

@ -22,7 +22,7 @@ data class BitchatPalette(
val isDark: Boolean,
// MARK: - Surfaces
/** Screen, header and composer background. Slightly green-tinted near-black. */
/** Screen, header and composer background. True black in dark mode. */
val background: Color,
/** Sheets, cards and list rows: one step above [background]. */
val surface: Color,
@ -56,7 +56,10 @@ data class BitchatPalette(
val DarkBitchatPalette = BitchatPalette(
isDark = true,
background = Color(0xFF070D07),
// True black. OLED panels switch these pixels off entirely, which is both the terminal
// aesthetic the app is going for and a real battery win on a screen that is mostly
// background.
background = Color(0xFF000000),
surface = Color(0xFF0E150E),
surfaceVariant = Color(0xFF182118),
outline = Color(0xFF2A3A2A),
@ -73,7 +76,7 @@ val DarkBitchatPalette = BitchatPalette(
val LightBitchatPalette = BitchatPalette(
isDark = false,
background = Color(0xFFFBFDFB),
background = Color(0xFFFFFFFF),
surface = Color(0xFFF2F6F2),
surfaceVariant = Color(0xFFE7EDE7),
outline = Color(0xFFCBD6CB),

View File

@ -75,7 +75,6 @@
<string name="chat_leave">مغادرة</string>
<string name="cd_nostr_reachable">يمكن الوصول عبر Nostr</string>
<string name="cd_unread_private_messages">رسائل خاصة غير مقروءة</string>
<string name="cd_toggle_bookmark">تبديل الإشارة المرجعية</string>
<string name="cd_teleported">تم الانتقال</string>
<string name="cd_tor_status">حالة Tor</string>
<string name="cd_connected_peers">الأقران المتصلون</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">ছেড়ে দিন</string>
<string name="cd_nostr_reachable">Nostr এর মাধ্যমে পৌঁছানো যায়</string>
<string name="cd_unread_private_messages">অপঠিত ব্যক্তিগত বার্তা</string>
<string name="cd_toggle_bookmark">বুকমার্ক টগল করুন</string>
<string name="cd_teleported">টেলিপোর্ট করা হয়েছে</string>
<string name="cd_tor_status">Tor অবস্থা</string>
<string name="cd_connected_peers">সংযুক্ত পিয়ার</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Lesezeichen umschalten</string>
<string name="cd_teleported">Teleportiert</string>
<string name="cd_tor_status">TorStatus</string>
<string name="cd_connected_peers">Verbundenen Peers</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Alternar marcador</string>
<string name="cd_teleported">Teletransportado</string>
<string name="cd_tor_status">Estado de Tor</string>
<string name="cd_connected_peers">Pares conectados</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">خروج</string>
<string name="cd_nostr_reachable">قابل دسترس از طریق Nostr</string>
<string name="cd_unread_private_messages">پیام‌های خصوصی خوانده‌نشده</string>
<string name="cd_toggle_bookmark">تغییر وضعیت نشانک</string>
<string name="cd_teleported">انتقال مکانی</string>
<string name="cd_tor_status">وضعیت Tor</string>
<string name="cd_connected_peers">همتایان متصل</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Itoggle ang bookmark</string>
<string name="cd_teleported">Naiteleport</string>
<string name="cd_tor_status">Katayuan ng Tor</string>
<string name="cd_connected_peers">Nakakonektang peers</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Basculer le signet</string>
<string name="cd_teleported">Téléporté</string>
<string name="cd_tor_status">Statut Tor</string>
<string name="cd_connected_peers">Pairs connectés</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">छोड़ें</string>
<string name="cd_nostr_reachable">Nostr के माध्यम से उपलब्ध</string>
<string name="cd_unread_private_messages">अपठित निजी संदेश</string>
<string name="cd_toggle_bookmark">बुकमार्क टॉगल करें</string>
<string name="cd_teleported">टेलीपोर्ट हुआ</string>
<string name="cd_tor_status">Tor स्थिति</string>
<string name="cd_connected_peers">कनेक्टेड पीयर्स</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Alihkan bookmark</string>
<string name="cd_teleported">Diteleportasi</string>
<string name="cd_tor_status">Status Tor</string>
<string name="cd_connected_peers">Peer yang terhubung</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Attiva/disattiva segnalibro</string>
<string name="cd_teleported">Teletrasportato</string>
<string name="cd_tor_status">Stato Tor</string>
<string name="cd_connected_peers">Peer connessi</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">退出</string>
<string name="cd_nostr_reachable">Nostr 経由で到達可能</string>
<string name="cd_unread_private_messages">未読のプライベートメッセージ</string>
<string name="cd_toggle_bookmark">ブックマークを切り替え</string>
<string name="cd_teleported">テレポート済み</string>
<string name="cd_tor_status">Tor ステータス</string>
<string name="cd_connected_peers">接続中のピア</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">გასვლა</string>
<string name="cd_nostr_reachable">მისაწვდომია Nostr-ით</string>
<string name="cd_unread_private_messages">წაუკითხავი პირადი შეტყობინებები</string>
<string name="cd_toggle_bookmark">სანიშნეს გადართვა</string>
<string name="cd_teleported">ტელეპორტირებული</string>
<string name="cd_tor_status">Tor-ის სტატუსი</string>
<string name="cd_connected_peers">დაკავშირებული პირები</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">나가기</string>
<string name="cd_nostr_reachable">Nostr로 연결 가능</string>
<string name="cd_unread_private_messages">읽지 않은 개인 메시지</string>
<string name="cd_toggle_bookmark">북마크 전환</string>
<string name="cd_teleported">텔레포트됨</string>
<string name="cd_tor_status">Tor 상태</string>
<string name="cd_connected_peers">연결된 피어</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Hanova bookmark</string>
<string name="cd_teleported">Nafindra toerana</string>
<string name="cd_tor_status">Toe-javatra Tor</string>
<string name="cd_connected_peers">Mpiara-miasa mifandray</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">छोड्नुहोस्</string>
<string name="cd_nostr_reachable">Nostr मार्फत पुग्न सकिने</string>
<string name="cd_unread_private_messages">नपढिएका निजी सन्देश</string>
<string name="cd_toggle_bookmark">बुकमार्क टगल</string>
<string name="cd_teleported">टेलिपोर्ट भयो</string>
<string name="cd_tor_status">Tor स्थिति</string>
<string name="cd_connected_peers">जडान भएका साथीहरू</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Bladwijzer omschakelen</string>
<string name="cd_teleported">Geteleporteerd</string>
<string name="cd_tor_status">Tor-status</string>
<string name="cd_connected_peers">Verbonden peers</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">باہر آؤ</string>
<string name="cd_nostr_reachable">Nostr راہین پہنچ ممکن اے</string>
<string name="cd_unread_private_messages">انہ پڑھیا ذاتی پیغام</string>
<string name="cd_toggle_bookmark">بُک مارک ٹوگل کرو</string>
<string name="cd_teleported">ٹیلپورٹ کیتا گیا</string>
<string name="cd_tor_status">Tor حالت</string>
<string name="cd_connected_peers">جُڑے پیئر</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Alternar favorito</string>
<string name="cd_teleported">Teletransportado</string>
<string name="cd_tor_status">Status do Tor</string>
<string name="cd_connected_peers">Peers conectados</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Alternar marcador</string>
<string name="cd_teleported">Teletransportado</string>
<string name="cd_tor_status">Estado do Tor</string>
<string name="cd_connected_peers">Pares ligados</string>

View File

@ -67,7 +67,6 @@
<string name="chat_leave">выйти</string>
<string name="cd_nostr_reachable">Доступен через Nostr</string>
<string name="cd_unread_private_messages">Непрочитанные личные</string>
<string name="cd_toggle_bookmark">Переключить закладку</string>
<string name="cd_teleported">Телепортировано</string>
<string name="cd_tor_status">Статус Tor</string>
<string name="cd_connected_peers">Подключённые узлы</string>

View File

@ -67,7 +67,6 @@
<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_toggle_bookmark">Växla bokmärke</string>
<string name="cd_teleported">Teleporterad</string>
<string name="cd_tor_status">Torstatus</string>
<string name="cd_connected_peers">Anslutna noder</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">ออก</string>
<string name="cd_nostr_reachable">ติดต่อได้ผ่าน Nostr</string>
<string name="cd_unread_private_messages">ข้อความส่วนตัวยังไม่ได้อ่าน</string>
<string name="cd_toggle_bookmark">สลับที่คั่นหน้า</string>
<string name="cd_teleported">เทเลพอร์ตแล้ว</string>
<string name="cd_tor_status">สถานะ Tor</string>
<string name="cd_connected_peers">เพียร์ที่เชื่อมต่อ</string>

View File

@ -67,7 +67,6 @@
<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_toggle_bookmark">Yer imini değiştir</string>
<string name="cd_teleported">Teleport edildi</string>
<string name="cd_tor_status">Tor durumu</string>
<string name="cd_connected_peers">Bağlı eşler</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">چھوڑیں</string>
<string name="cd_nostr_reachable">Nostr کے ذریعے رسائی</string>
<string name="cd_unread_private_messages">غیر پڑھے ہوئے نجی پیغامات</string>
<string name="cd_toggle_bookmark">بک مارک تبدیل کریں</string>
<string name="cd_teleported">ٹیلی پورٹ</string>
<string name="cd_tor_status">Tor کی کیفیت</string>
<string name="cd_connected_peers">منسلک پیئرز</string>

View File

@ -75,7 +75,6 @@
<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_toggle_bookmark">Bật/tắt bookmark</string>
<string name="cd_teleported">Đã dịch chuyển</string>
<string name="cd_tor_status">Trạng thái Tor</string>
<string name="cd_connected_peers">Peers đã kết nối</string>

View File

@ -75,7 +75,6 @@
<string name="chat_leave">离开</string>
<string name="cd_nostr_reachable">可通过 Nostr 联系</string>
<string name="cd_unread_private_messages">未读私信</string>
<string name="cd_toggle_bookmark">切换书签</string>
<string name="cd_teleported">已传送</string>
<string name="cd_tor_status">Tor 状态</string>
<string name="cd_connected_peers">已连接节点</string>

View File

@ -76,7 +76,6 @@
<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_toggle_bookmark">Toggle bookmark</string>
<string name="cd_location_notes">Location notes</string>
<string name="cd_teleported">Teleported</string>
<string name="cd_tor_status">Tor status</string>
@ -111,8 +110,8 @@
<string name="about_tagline">The sidegroup chat</string>
<!-- About sheet: tabs -->
<string name="about_tab_how_to_use">How to use</string>
<string name="about_tab_basic_info">Basic info</string>
<string name="about_tab_info">Info</string>
<string name="about_tab_settings">Settings</string>
<!-- About sheet: how-to-use instructions -->
<string name="about_how_to_use_heading">How To Use</string>