mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-15 06:56:30 +00:00
* first pass * pass 2 * cleanup * capitalization * strings * input bar fixes * fixes * notes * nice * nicer * lists * cleanup * button * fixes * animations * Fix layout jumpiness in chat and geohash people list Three separate causes of things moving when they should not: - Chat lurched whenever a bottom sheet closed. Placement animation is meant to soften insertions and removals, but any relayout moves every item -- a sheet's text field opening the keyboard changes the chat's IME inset, and closing it changes it back. Placement animation is now armed only briefly around a real change to the message list, so items otherwise track the viewport exactly. - Anon list changed height as participants churned. Rows sized to their content, so any reorder could change the card's height; and the card sized to the live anon count, which moves constantly in a busy geohash. Rows now have an exact height, and a trimmed anon card reserves the full capped height regardless of how many are present beyond the cap. - Anons are now their own trailing section rather than a tail on each of "on location" and "teleported in", which had pushed the few recognisable names out of view twice over. Self is never grouped as an anon. Adds 7 tests covering the sectioning and the fixed-length behaviour. * Group geohash people as People and Anon Replaces the "on location" / "teleported in" / "anonymous" split with two sections: peers who announced a nickname, then the anons. Teleport state was never worth a section of its own -- every row already carries it as a distinct glyph -- and splitting on it fragmented the short list people actually read, in a channel where most participants are anonymous anyway. Self stays in the People section even when unnamed. * Key message list state per conversation Switching channels reused every piece of state in MessagesList, because none of it was keyed on which conversation was being shown: - The LazyListState carried the previous channel's scroll offset, so the new channel opened at a stale position and then corrected itself. - hasScrolledToInitialPosition and followIncomingMessages carried over, so a channel entered after scrolling up in another one did not land on its newest message at all. - The arrival tracker had never seen the incoming channel's ids, so a backlog of six or fewer messages was treated as six simultaneous arrivals and each one slid in. - previousMessageCount carried over, arming placement animation for the relayout that the switch itself caused. All of it is now keyed on a conversationKey derived the same way displayMessages is. The tracker also detects a list sharing no ids with the previous one and adopts it silently, which covers /clear and any caller that does not supply a distinct key. Adds 4 tests for wholesale replacement, including the case that the burst cap cannot catch on its own. * fix location channel layout * icon * location sheet * move location error * fix location channel lifecycle bug * remove empty lable * geist mono * timestamp no seconds * new icons * icons * cleanup * mentions * fix mentions * grouping of geohash channel list * colors * fix mention colors * Bring private and group chat headers up to the main header's layout Both conversation headers were built on TopAppBar with a centred title, a back arrow on the left and everything else crowded into the title slot, at 14sp with 14dp icons. Moving between the timeline and a conversation visibly shifted the bar's height, insets and type. Introduces ConversationHeader, built from the main header's own tokens rather than TopAppBar: same ChatHeaderHeight, same 12/8dp edge insets, leading glyph in a 44dp slot so it lands exactly where the brand mark does, same -6dp optical nudge pulling the title toward it, same 17sp label. - Drops the back button; the close action on the right is the way out. Leaving a channel outright already lives on its row in the network sheet, so it does not need a second home beside the exit. - Leading glyph is the transport: globe over the internet, wifi/bluetooth/ routed on the mesh, matching the main header's channel button. - Actions are right-aligned and unweighted -- favourite, encryption state, close -- so a long title yields space to them instead of pushing them off screen. - Private chat titles use the primary green like every other header label, rather than orange for Nostr-reachable peers. Height and edge insets now belong to each header variant instead of the ChatFloatingHeader wrapper, which was applying them a second time to the channel header. Adds nine spec icons in the existing 20x20 / 1.25-stroke language -- bluetooth, wifi, routed, close, check, warning, sync, lock_open, envelope -- so the headers and peer rows no longer mix Material glyphs into the set. * color
231 lines
10 KiB
Kotlin
231 lines
10 KiB
Kotlin
package com.bitchat.android.ui
|
|
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.graphics.Color
|
|
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.BitchatFontFamily
|
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
|
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
|
import androidx.compose.ui.res.stringResource
|
|
import com.bitchat.android.R
|
|
import androidx.compose.ui.platform.LocalClipboardManager
|
|
import androidx.compose.ui.text.AnnotatedString
|
|
import com.bitchat.android.core.ui.component.sheet.BitchatBottomSheet
|
|
import com.bitchat.android.model.BitchatMessage
|
|
|
|
/**
|
|
* User Action Sheet for selecting actions on a specific user (slap, hug, block)
|
|
* Design language matches LocationChannelsSheet.kt for consistency
|
|
*/
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun ChatUserSheet(
|
|
isPresented: Boolean,
|
|
onDismiss: () -> Unit,
|
|
targetNickname: String,
|
|
selectedMessage: BitchatMessage? = null,
|
|
viewModel: ChatViewModel,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
val coroutineScope = rememberCoroutineScope()
|
|
val clipboardManager = LocalClipboardManager.current
|
|
|
|
val colorScheme = MaterialTheme.colorScheme
|
|
val palette = LocalBitchatPalette.current
|
|
val standardGreen = colorScheme.primary
|
|
val standardBlue = colorScheme.secondary
|
|
val standardPurple = palette.accentPurple
|
|
val standardRed = colorScheme.error
|
|
val standardGrey = colorScheme.onSurfaceVariant
|
|
|
|
if (isPresented) {
|
|
BitchatBottomSheet(
|
|
onDismissRequest = onDismiss,
|
|
modifier = modifier
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
// Header
|
|
Text(
|
|
text = stringResource(R.string.at_nickname, targetNickname),
|
|
fontSize = 18.sp,
|
|
fontFamily = BitchatFontFamily,
|
|
fontWeight = FontWeight.Bold,
|
|
color = MaterialTheme.colorScheme.onSurface
|
|
)
|
|
|
|
Text(
|
|
text = if (selectedMessage != null) stringResource(R.string.choose_action_message_or_user) else stringResource(R.string.choose_action_user),
|
|
fontSize = 12.sp,
|
|
fontFamily = BitchatFontFamily,
|
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
|
)
|
|
|
|
// Action list (iOS-style plain list)
|
|
LazyColumn(
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
// Copy message action (only show if we have a message)
|
|
selectedMessage?.let { message ->
|
|
item {
|
|
UserActionRow(
|
|
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
|
|
clipboardManager.setText(AnnotatedString(message.content))
|
|
onDismiss()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
// Only show user actions for other users' messages or when no message is selected
|
|
if (selectedMessage?.sender != viewModel.nickname.value) {
|
|
// Send private message action
|
|
item {
|
|
UserActionRow(
|
|
title = stringResource(R.string.action_private_message_title, targetNickname),
|
|
subtitle = stringResource(R.string.action_private_message_subtitle),
|
|
titleColor = standardPurple,
|
|
onClick = {
|
|
val selectedLocationChannel = viewModel.selectedLocationChannel.value
|
|
if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location) {
|
|
if (selectedMessage?.senderPeerID?.startsWith("nostr:") == true) {
|
|
val shortId = selectedMessage.senderPeerID!!.substring(6)
|
|
viewModel.startGeohashDMByShortId(shortId)
|
|
} else {
|
|
viewModel.startGeohashDMByNickname(targetNickname)
|
|
}
|
|
} else {
|
|
// Mesh chat
|
|
val peerID = selectedMessage?.senderPeerID ?: viewModel.getPeerIDForNickname(targetNickname)
|
|
if (peerID != null) {
|
|
viewModel.showPrivateChatSheet(peerID)
|
|
}
|
|
}
|
|
onDismiss()
|
|
}
|
|
)
|
|
}
|
|
|
|
// Slap action
|
|
item {
|
|
UserActionRow(
|
|
title = stringResource(R.string.action_slap_title, targetNickname),
|
|
subtitle = stringResource(R.string.action_slap_subtitle),
|
|
titleColor = standardBlue,
|
|
onClick = {
|
|
// Send slap command
|
|
viewModel.sendMessage("/slap $targetNickname")
|
|
onDismiss()
|
|
}
|
|
)
|
|
}
|
|
|
|
// Hug action
|
|
item {
|
|
UserActionRow(
|
|
title = stringResource(R.string.action_hug_title, targetNickname),
|
|
subtitle = stringResource(R.string.action_hug_subtitle),
|
|
titleColor = standardGreen,
|
|
onClick = {
|
|
// Send hug command
|
|
viewModel.sendMessage("/hug $targetNickname")
|
|
onDismiss()
|
|
}
|
|
)
|
|
}
|
|
|
|
// Block action
|
|
item {
|
|
UserActionRow(
|
|
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
|
|
val selectedLocationChannel = viewModel.selectedLocationChannel.value
|
|
if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location) {
|
|
// Get user's nostr public key and add to geohash block list
|
|
viewModel.blockUserInGeohash(targetNickname)
|
|
} else {
|
|
// Regular mesh blocking
|
|
viewModel.sendMessage("/block $targetNickname")
|
|
}
|
|
onDismiss()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cancel button (iOS-style)
|
|
Button(
|
|
onClick = onDismiss,
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
|
|
contentColor = MaterialTheme.colorScheme.onSurface
|
|
),
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Text(
|
|
text = stringResource(R.string.cancel_lower),
|
|
fontSize = BASE_FONT_SIZE.sp,
|
|
fontFamily = BitchatFontFamily
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun UserActionRow(
|
|
title: String,
|
|
subtitle: String,
|
|
titleColor: Color,
|
|
onClick: () -> Unit
|
|
) {
|
|
// iOS-style list row (plain button, no card background)
|
|
Surface(
|
|
onClick = onClick,
|
|
color = Color.Transparent,
|
|
shape = MaterialTheme.shapes.medium,
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
) {
|
|
Text(
|
|
text = title,
|
|
fontSize = BASE_FONT_SIZE.sp,
|
|
fontFamily = BitchatFontFamily,
|
|
fontWeight = FontWeight.Medium,
|
|
color = titleColor
|
|
)
|
|
|
|
Text(
|
|
text = subtitle,
|
|
fontSize = 12.sp,
|
|
fontFamily = BitchatFontFamily,
|
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
|
)
|
|
}
|
|
}
|
|
}
|