Merge branch 'main' of ssh://github.com/permissionlesstech/bitchat-android into prudhvir3ddy/upgrade_deps

This commit is contained in:
prudhvir3ddy
2025-07-12 10:16:12 +05:30
20 changed files with 403 additions and 122 deletions
+2 -1
View File
@@ -24,7 +24,8 @@ android {
buildTypes {
release {
isMinifyEnabled = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -30,10 +30,11 @@ fun PermissionExplanationScreen(
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp)
.padding(horizontal = 24.dp)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(modifier = Modifier.height(24.dp))
// Header
Column(
modifier = Modifier.fillMaxWidth(),
@@ -164,6 +165,7 @@ fun PermissionExplanationScreen(
)
}
}
Spacer(modifier = Modifier.height(24.dp))
}
}
@@ -284,9 +284,21 @@ class ChannelManager(
state.setShowPasswordPrompt(false)
state.setPasswordPromptChannel(null)
}
fun setChannelPassword(channel: String, password: String): Boolean {
return verifyChannelPassword(channel, password)
fun setChannelPassword(channel: String, password: String) {
channelPasswords[channel] = password
channelKeys[channel] = deriveChannelKey(password, channel)
state.setPasswordProtectedChannels(
state.getPasswordProtectedChannelsValue().toMutableSet().apply { add(channel) }
)
dataManager.saveChannelData(
state.getJoinedChannelsValue(),
state.getPasswordProtectedChannelsValue()
)
}
// MARK: - Emergency Clear
@@ -103,7 +103,7 @@ fun PeerCounter(
imageVector = Icons.Filled.Email,
contentDescription = "Unread private messages",
modifier = Modifier.size(16.dp),
tint = Color(0xFFFF8C00) // Orange to match private message theme
tint = Color(0xFFFF9500) // Orange to match private message theme
)
Spacer(modifier = Modifier.width(6.dp))
}
@@ -201,19 +201,18 @@ private fun PrivateChatHeader(
val colorScheme = MaterialTheme.colorScheme
val peerNickname = peerNicknames[peerID] ?: peerID
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Fixed: Make back button wider to prevent text cropping
Box(modifier = Modifier.fillMaxWidth()) {
// Back button - positioned all the way to the left with minimal margin
Button(
onClick = onBackClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = colorScheme.primary
),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding
modifier = Modifier
.align(Alignment.CenterStart)
.offset(x = (-8).dp) // Move even further left to minimize margin
) {
Row(
verticalAlignment = Alignment.CenterVertically
@@ -233,27 +232,30 @@ private fun PrivateChatHeader(
}
}
Spacer(modifier = Modifier.weight(1f))
Row(verticalAlignment = Alignment.CenterVertically) {
// Title - perfectly centered regardless of other elements
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.Center)
) {
Icon(
imageVector = Icons.Filled.Lock,
contentDescription = "Private chat",
modifier = Modifier.size(16.dp),
tint = Color(0xFFFF8C00) // Orange to match private message theme
tint = Color(0xFFFF9500) // Orange to match private message theme
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = peerNickname,
style = MaterialTheme.typography.titleMedium,
color = Color(0xFFFF8C00) // Orange
color = Color(0xFFFF9500) // Orange
)
}
Spacer(modifier = Modifier.weight(1f))
// Favorite button with proper filled/outlined star
IconButton(onClick = onToggleFavorite) {
// Favorite button - positioned on the right
IconButton(
onClick = onToggleFavorite,
modifier = Modifier.align(Alignment.CenterEnd)
) {
Icon(
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
@@ -273,12 +275,19 @@ private fun ChannelHeader(
) {
val colorScheme = MaterialTheme.colorScheme
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBackClick) {
Box(modifier = Modifier.fillMaxWidth()) {
// Back button - positioned all the way to the left with minimal margin
Button(
onClick = onBackClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = colorScheme.primary
),
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding
modifier = Modifier
.align(Alignment.CenterStart)
.offset(x = (-8).dp) // Move even further left to minimize margin
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
@@ -297,18 +306,21 @@ private fun ChannelHeader(
}
}
Spacer(modifier = Modifier.weight(1f))
// Title - perfectly centered regardless of other elements
Text(
text = "channel: $channel",
style = MaterialTheme.typography.titleMedium,
color = Color(0xFF0080FF), // Blue
modifier = Modifier.clickable { onSidebarClick() }
color = Color(0xFFFF9500), // Orange to match input field
modifier = Modifier
.align(Alignment.Center)
.clickable { onSidebarClick() }
)
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onLeaveChannel) {
// Leave button - positioned on the right
TextButton(
onClick = onLeaveChannel,
modifier = Modifier.align(Alignment.CenterEnd)
) {
Text(
text = "leave",
style = MaterialTheme.typography.bodySmall,
@@ -139,7 +139,7 @@ private fun appendFormattedContent(
}
"mention" -> {
builder.pushStyle(SpanStyle(
color = Color(0xFFFF8C00), // Orange
color = Color(0xFFFF9500), // Orange
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
))
@@ -37,8 +37,9 @@ class CommandProcessor(
when (cmd) {
"/j", "/join" -> handleJoinCommand(parts, myPeerID)
"/m", "/msg" -> handleMessageCommand(parts, meshService)
"/w" -> handleWhoCommand()
"/w" -> handleWhoCommand(meshService)
"/clear" -> handleClearCommand()
"/pass" -> handlePassCommand(parts, myPeerID)
"/block" -> handleBlockCommand(parts, meshService)
"/unblock" -> handleUnblockCommand(parts, meshService)
"/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage)
@@ -54,7 +55,8 @@ class CommandProcessor(
if (parts.size > 1) {
val channelName = parts[1]
val channel = if (channelName.startsWith("#")) channelName else "#$channelName"
val success = channelManager.joinChannel(channel, null, myPeerID)
val password = if (parts.size > 2) parts[2] else null
val success = channelManager.joinChannel(channel, password, myPeerID)
if (success) {
val systemMessage = BitchatMessage(
sender = "system",
@@ -127,11 +129,11 @@ class CommandProcessor(
}
}
private fun handleWhoCommand() {
private fun handleWhoCommand(meshService: Any) {
val connectedPeers = state.getConnectedPeersValue()
val peerList = connectedPeers.joinToString(", ") { peerID ->
// This would need mesh service access for nicknames
peerID // For now just use peer ID
// Convert peerID to nickname using the mesh service
getPeerNickname(peerID, meshService)
}
val systemMessage = BitchatMessage(
@@ -165,6 +167,52 @@ class CommandProcessor(
}
}
}
private fun handlePassCommand(parts: List<String>, peerID: String) {
val currentChannel = state.getCurrentChannelValue()
if (currentChannel == null) {
val systemMessage = BitchatMessage(
sender = "system",
content = "you must be in a channel to set a password.",
timestamp = Date(),
isRelay = false
)
messageManager.addMessage(systemMessage)
return
}
if (parts.size == 2){
if(!channelManager.isChannelCreator(channel = currentChannel, peerID = peerID)){
val systemMessage = BitchatMessage(
sender = "system",
content = "you must be the channel creator to set a password.",
timestamp = Date(),
isRelay = false
)
channelManager.addChannelMessage(currentChannel,systemMessage,null)
return
}
val newPassword = parts[1]
channelManager.setChannelPassword(currentChannel, newPassword)
val systemMessage = BitchatMessage(
sender = "system",
content = "password changed for channel $currentChannel",
timestamp = Date(),
isRelay = false
)
channelManager.addChannelMessage(currentChannel,systemMessage,null)
}
else{
val systemMessage = BitchatMessage(
sender = "system",
content = "usage: /pass <password>",
timestamp = Date(),
isRelay = false
)
channelManager.addChannelMessage(currentChannel,systemMessage,null)
}
}
private fun handleBlockCommand(parts: List<String>, meshService: Any) {
if (parts.size > 1) {
@@ -44,20 +44,17 @@ fun MessageInput(
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
verticalAlignment = Alignment.CenterVertically
) {
// Fixed: Remove arrow from private message input
// Remove arrow from both private and channel inputs to match DM style
Text(
text = when {
selectedPrivatePeer != null -> "<@$nickname>" // Removed arrow for private
currentChannel != null -> "<@$nickname> →" // Keep arrow for channels
else -> "<@$nickname>"
},
text = "<@$nickname>", // No arrow for both private and channel
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
color = when {
selectedPrivatePeer != null -> Color(0xFFFF8C00) // Orange for private
currentChannel != null -> Color(0xFFFF8C00) // Orange if encrypted channel
selectedPrivatePeer != null -> Color(0xFFFF9500) // Orange for private
currentChannel != null -> Color(0xFFFF9500) // Orange for channels too
else -> colorScheme.primary
},
fontFamily = FontFamily.Monospace
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
)
Spacer(modifier = Modifier.width(8.dp))
@@ -78,7 +75,7 @@ fun MessageInput(
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
// Fixed: Make send button orange in private mode to match nickname color
// Update send button to match input field colors
IconButton(
onClick = onSend,
modifier = Modifier.size(32.dp)
@@ -87,9 +84,9 @@ fun MessageInput(
modifier = Modifier
.size(30.dp)
.background(
color = if (selectedPrivatePeer != null) {
// Orange for private messages to match nickname color
Color(0xFFFF8C00).copy(alpha = 0.75f)
color = if (selectedPrivatePeer != null || currentChannel != null) {
// Orange for both private messages and channels to match nickname color
Color(0xFFFF9500).copy(alpha = 0.75f)
} else if (colorScheme.background == Color.Black) {
Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme
} else {
@@ -100,11 +97,11 @@ fun MessageInput(
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.KeyboardArrowUp,
imageVector = Icons.Filled.KeyboardArrowRight,
contentDescription = "Send message",
modifier = Modifier.size(20.dp),
tint = if (selectedPrivatePeer != null) {
// Black arrow on orange in private mode
tint = if (selectedPrivatePeer != null || currentChannel != null) {
// Black arrow on orange for both private and channel modes
Color.Black
} else if (colorScheme.background == Color.Black) {
Color.Black // Black arrow on bright green in dark theme
@@ -193,7 +193,7 @@ fun ChannelsSection(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "#$channel",
text = channel, // Channel already contains the # prefix
style = MaterialTheme.typography.bodyMedium,
color = if (isSelected) colorScheme.primary else colorScheme.onSurface,
fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal,
@@ -320,7 +320,7 @@ private fun PeerItem(
imageVector = Icons.Filled.Email,
contentDescription = "Unread messages",
modifier = Modifier.size(16.dp),
tint = Color(0xFFFF8C00) // Orange to match private message theme
tint = Color(0xFFFF9500) // Orange to match private message theme
)
} else {
// Signal strength indicators
@@ -0,0 +1,60 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
>
<path
android:name="path"
android:pathData="M 40 45 L 68 45 C 70.2 45 72 46.8 72 49 L 72 59 C 72 61.2 70.2 63 68 63 L 40 63 C 37.8 63 36 61.2 36 59 L 36 49 C 36 46.8 37.8 45 40 45 Z"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeWidth="2"
/>
<path
android:name="path_1"
android:pathData="M 41 47 L 67 47 C 68.1 47 69 47.9 69 49 L 69 57 C 69 58.1 68.1 59 67 59 L 41 59 C 39.9 59 39 58.1 39 57 L 39 49 C 39 47.9 39.9 47 41 47 Z"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeWidth="2"
/>
<path
android:name="path_2"
android:pathData="M 43 51 L 46 51 L 46 55 L 43 55 Z"
android:fillColor="#000000"
/>
<path
android:name="path_3"
android:pathData="M 48 52 L 64 52 L 64 53.5 L 48 53.5 Z"
android:fillColor="#000000"
/>
<path
android:name="path_4"
android:pathData="M 48 54.5 L 60 54.5 L 60 56 L 48 56 Z"
android:fillColor="#000000"
/>
<path
android:name="path_5"
android:pathData="M 62 38 M 60.5 38 C 60.5 37.602 60.658 37.221 60.939 36.939 C 61.221 36.658 61.602 36.5 62 36.5 C 62.398 36.5 62.779 36.658 63.061 36.939 C 63.342 37.221 63.5 37.602 63.5 38 C 63.5 38.398 63.342 38.779 63.061 39.061 C 62.779 39.342 62.398 39.5 62 39.5 C 61.602 39.5 61.221 39.342 60.939 39.061 C 60.658 38.779 60.5 38.398 60.5 38"
android:fillColor="#000000"
/>
<path
android:name="path_6"
android:pathData="M 58.5 35 M 57.5 35 C 57.5 34.735 57.605 34.48 57.793 34.293 C 57.98 34.105 58.235 34 58.5 34 C 58.765 34 59.02 34.105 59.207 34.293 C 59.395 34.48 59.5 34.735 59.5 35 C 59.5 35.265 59.395 35.52 59.207 35.707 C 59.02 35.895 58.765 36 58.5 36 C 58.235 36 57.98 35.895 57.793 35.707 C 57.605 35.52 57.5 35.265 57.5 35"
android:fillColor="#000000"
/>
<path
android:name="path_7"
android:pathData="M 65.5 35 M 64.5 35 C 64.5 34.735 64.605 34.48 64.793 34.293 C 64.98 34.105 65.235 34 65.5 34 C 65.765 34 66.02 34.105 66.207 34.293 C 66.395 34.48 66.5 34.735 66.5 35 C 66.5 35.265 66.395 35.52 66.207 35.707 C 66.02 35.895 65.765 36 65.5 36 C 65.235 36 64.98 35.895 64.793 35.707 C 64.605 35.52 64.5 35.265 64.5 35"
android:fillColor="#000000"
/>
<path
android:name="path_8"
android:pathData="M 59 35.5 L 62.5 38 L 65 35.5"
android:fillColor="#55000000"
android:strokeColor="#000000"
android:strokeWidth="1"
/>
</vector>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
@@ -0,0 +1,78 @@
package com.bitchat
import androidx.compose.ui.graphics.Color
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Generate a consistent color for a username based on their peer ID or nickname
* Returns colors that work well on both light and dark backgrounds
*/
class ColorTest {
fun getUsernameColor(identifier: String): Color {
// Hash the identifier to get a consistent number
val hash = identifier.hashCode().toUInt()
// Terminal-friendly colors that work on both black and white backgrounds
val colors = listOf(
Color(0xFF00FF00), // Bright Green
Color(0xFF00FFFF), // Cyan
Color(0xFFFFFF00), // Yellow
Color(0xFFFF00FF), // Magenta
Color(0xFF0080FF), // Bright Blue
Color(0xFFFF8000), // Orange
Color(0xFF80FF00), // Lime Green
Color(0xFF8000FF), // Purple
Color(0xFFFF0080), // Pink
Color(0xFF00FF80), // Spring Green
Color(0xFF80FFFF), // Light Cyan
Color(0xFFFF8080), // Light Red
Color(0xFF8080FF), // Light Blue
Color(0xFFFFFF80), // Light Yellow
Color(0xFFFF80FF), // Light Magenta
Color(0xFF80FF80), // Light Green
)
// Use modulo to get consistent color for same identifier
return colors[(hash % colors.size.toUInt()).toInt()]
}
@Test
fun is_username_derived_color_consistent() {
println("Testing username color function:")
val testUsers = listOf("alice", "bob", "charlie", "diana", "eve")
testUsers.forEach { user ->
val color = getUsernameColor(user)
println("User '$user' gets color: ${color.value.toString(16).uppercase()}")
}
val `alice'sColor` = getUsernameColor(testUsers[0])
val `bob'sColor` = getUsernameColor(testUsers[1])
val `charlie'sColor` = getUsernameColor(testUsers[2])
val `diana'sColor` = getUsernameColor(testUsers[3])
val `eve'sColor` = getUsernameColor(testUsers[4])
// Test consistency - same user should always get same color
println("\nTesting consistency:")
repeat(3) {
val `alice's_color` = getUsernameColor(testUsers[0])
val `bob's_color` = getUsernameColor(testUsers[1])
val `charlie's_color` = getUsernameColor(testUsers[2])
val `diana's_color` = getUsernameColor(testUsers[3])
val `eve's_color` = getUsernameColor(testUsers[4])
assertEquals(`alice'sColor`, `alice's_color`)
assertEquals(`bob'sColor`, `bob's_color`)
assertEquals(`charlie'sColor`, `charlie's_color`)
assertEquals(`diana'sColor`, `diana's_color`)
assertEquals(`eve'sColor`, `eve's_color`)
println("Alice color (test ${it + 1}): ${`alice'sColor`.value.toString(16).uppercase()}")
}
}
}