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 -2
View File
@@ -9,11 +9,11 @@ body:
attributes: attributes:
label: "Checklist" label: "Checklist"
options: options:
- label: "I made sure that there are *no existing issues or discussions* - [open](https://github.com/callebtc/bitchat-android/issues) or [closed](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." - label: "I made sure that there are *no existing issues or discussions* - [open](https://github.com/permissionlesstech/bitchat-android/issues) or [closed](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true required: true
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise." - label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
required: true required: true
- label: "I have read and understood the [technical architecture](https://github.com/callebtc/bitchat-android/blob/main/README.md#technical-architecture)." - label: "I have read and understood the [technical architecture](https://github.com/permissionlesstech/bitchat-android/blob/main/README.md#technical-architecture)."
required: true required: true
- type: textarea - type: textarea
+3 -3
View File
@@ -14,15 +14,15 @@ body:
attributes: attributes:
label: "Checklist" label: "Checklist"
options: options:
- label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/callebtc/bitchat-android/releases/latest)." - label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/permissionlesstech/bitchat-android/releases/latest)."
required: true required: true
- label: "I made sure that there are *no existing issues* - [open](https://github.com/callebtc/bitchat-android/issues) or [closed](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." - label: "I made sure that there are *no existing issues* - [open](https://github.com/permissionlesstech/bitchat-android/issues) or [closed](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true required: true
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise." - label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
required: true required: true
- label: "This issue contains only one bug." - label: "This issue contains only one bug."
required: true required: true
- label: "I have read and understood the [contribution guidelines](https://github.com/callebtc/bitchat-android/blob/main/README.md#contributing)." - label: "I have read and understood the [contribution guidelines](https://github.com/permissionlesstech/bitchat-android/blob/main/README.md#contributing)."
required: true required: true
- type: input - type: input
+2 -2
View File
@@ -7,9 +7,9 @@ body:
attributes: attributes:
label: Checklist label: Checklist
options: options:
- label: I have used the search function for [**OPEN**](https://github.com/callebtc/bitchat-android/issues) issues to see if someone else has already submitted the same feature request. - label: I have used the search function for [**OPEN**](https://github.com/permissionlesstech/bitchat-android/issues) issues to see if someone else has already submitted the same feature request.
required: true required: true
- label: I have **also** used the search function for [**CLOSED**](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) issues to see if the feature was already implemented and is just waiting to be released, or if the feature was rejected. - label: I have **also** used the search function for [**CLOSED**](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) issues to see if the feature was already implemented and is just waiting to be released, or if the feature was rejected.
required: true required: true
- label: I will describe the problem with as much detail as possible. - label: I will describe the problem with as much detail as possible.
required: true required: true
+1 -1
View File
@@ -6,7 +6,7 @@
please make sure that you have done all of the following. please make sure that you have done all of the following.
You can tick the boxes below by placing an x inside the brackets like this: [x] You can tick the boxes below by placing an x inside the brackets like this: [x]
--> -->
- [ ] I have read the contribution guidelines: <https://github.com/callebtc/bitchat-android?tab=readme-ov-file#contributing> - [ ] I have read the contribution guidelines: <https://github.com/permissionlesstech/bitchat-android?tab=readme-ov-file#contributing>
- [ ] I have performed a self-review of my code - [ ] I have performed a self-review of my code
<!-- - [ ] I have run the automated code checks using `./gradlew checkstyle spotbugsPlayDebug spotbugsDebug :app:lintPlayDebug` --> <!-- - [ ] I have run the automated code checks using `./gradlew checkstyle spotbugsPlayDebug spotbugsDebug :app:lintPlayDebug` -->
- [ ] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see <https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue>) - [ ] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see <https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue>)
+29
View File
@@ -0,0 +1,29 @@
name: Android Build
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Build with Gradle
run: ./gradlew build
+91
View File
@@ -0,0 +1,91 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- `/w` command now displays user nicknames instead of peer IDs
## [0.5.1] - 2025-07-10
### Added
- Bluetooth startup check with user prompt to enable Bluetooth if disabled
### Fixed
- Improved Bluetooth initialization reliability on first app launch
## [0.5] - 2025-07-10
### Added
- New user onboarding screen with permission explanations
- Educational content explaining why each permission is required
- Privacy assurance messaging (no tracking, no servers, local-only data)
### Fixed
- Comprehensive permission validation - ensures all required permissions are granted
- Proper Bluetooth stack initialization on first app load
- Eliminated need for manual app restart after installation
- Enhanced permission request coordination and error handling
### Changed
- Improved first-time user experience with guided setup flow
## [0.4] - 2025-07-10
### Added
- Push notifications for direct messages
- Enhanced notification system with proper click handling and grouping
### Improved
- Direct message (DM) view with better user interface
- Enhanced private messaging experience
### Known Issues
- Favorite peer functionality currently broken
## [0.3] - 2025-07-09
### Added
- Battery-aware scanning policies for improved power management
- Dynamic scan behavior based on device battery state
### Fixed
- Android-to-Android Bluetooth Low Energy connections
- Peer discovery reliability between Android devices
- Connection stability improvements
## [0.2] - 2025-07-09
### Added
- Initial Android implementation of bitchat protocol
- Bluetooth Low Energy mesh networking
- End-to-end encryption for private messages
- Channel-based messaging with password protection
- Store-and-forward message delivery
- IRC-style commands (/msg, /join, /clear, etc.)
- RSSI-based signal quality indicators
### Fixed
- Various Bluetooth handling improvements
- User interface refinements
- Connection reliability enhancements
## [0.1] - 2025-07-08
### Added
- Initial release of bitchat Android client
- Basic mesh networking functionality
- Core messaging features
- Protocol compatibility with iOS bitchat client
[Unreleased]: https://github.com/permissionlesstech/bitchat-android/compare/0.5.1...HEAD
[0.5.1]: https://github.com/permissionlesstech/bitchat-android/compare/0.5...0.5.1
[0.5]: https://github.com/permissionlesstech/bitchat-android/compare/0.4...0.5
[0.4]: https://github.com/permissionlesstech/bitchat-android/compare/0.3...0.4
[0.3]: https://github.com/permissionlesstech/bitchat-android/compare/0.2...0.3
[0.2]: https://github.com/permissionlesstech/bitchat-android/compare/0.1...0.2
[0.1]: https://github.com/permissionlesstech/bitchat-android/releases/tag/0.1
+2 -2
View File
@@ -13,7 +13,7 @@ This is the **Android port** of the original [bitchat iOS app](https://github.co
## Install bitchat ## Install bitchat
You can download the latest version of bitchat for Android from the [GitHub Releases page](https://github.com/callebtc/bitchat-android/releases). You can download the latest version of bitchat for Android from the [GitHub Releases page](https://github.com/permissionlesstech/bitchat-android/releases).
**Instructions:** **Instructions:**
@@ -53,7 +53,7 @@ This project is released into the public domain. See the [LICENSE](LICENSE.md) f
1. **Clone the repository:** 1. **Clone the repository:**
```bash ```bash
git clone https://github.com/your-username/bitchat-android.git git clone https://github.com/permissionlesstech/bitchat-android.git
cd bitchat-android cd bitchat-android
``` ```
+2 -1
View File
@@ -24,7 +24,8 @@ android {
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = true
isShrinkResources = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
@@ -30,10 +30,11 @@ fun PermissionExplanationScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(24.dp) .padding(horizontal = 24.dp)
.verticalScroll(scrollState), .verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Spacer(modifier = Modifier.height(24.dp))
// Header // Header
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -164,6 +165,7 @@ fun PermissionExplanationScreen(
) )
} }
} }
Spacer(modifier = Modifier.height(24.dp))
} }
} }
@@ -285,8 +285,20 @@ class ChannelManager(
state.setPasswordPromptChannel(null) state.setPasswordPromptChannel(null)
} }
fun setChannelPassword(channel: String, password: String): Boolean { fun setChannelPassword(channel: String, password: String) {
return verifyChannelPassword(channel, password)
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 // MARK: - Emergency Clear
@@ -103,7 +103,7 @@ fun PeerCounter(
imageVector = Icons.Filled.Email, imageVector = Icons.Filled.Email,
contentDescription = "Unread private messages", contentDescription = "Unread private messages",
modifier = Modifier.size(16.dp), 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)) Spacer(modifier = Modifier.width(6.dp))
} }
@@ -201,19 +201,18 @@ private fun PrivateChatHeader(
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val peerNickname = peerNicknames[peerID] ?: peerID val peerNickname = peerNicknames[peerID] ?: peerID
Row( Box(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(), // Back button - positioned all the way to the left with minimal margin
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Fixed: Make back button wider to prevent text cropping
Button( Button(
onClick = onBackClick, onClick = onBackClick,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentColor = colorScheme.primary 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( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -233,27 +232,30 @@ private fun PrivateChatHeader(
} }
} }
Spacer(modifier = Modifier.weight(1f)) // Title - perfectly centered regardless of other elements
Row(
Row(verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.Center)
) {
Icon( Icon(
imageVector = Icons.Filled.Lock, imageVector = Icons.Filled.Lock,
contentDescription = "Private chat", contentDescription = "Private chat",
modifier = Modifier.size(16.dp), 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)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = peerNickname, text = peerNickname,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = Color(0xFFFF8C00) // Orange color = Color(0xFFFF9500) // Orange
) )
} }
Spacer(modifier = Modifier.weight(1f)) // Favorite button - positioned on the right
IconButton(
// Favorite button with proper filled/outlined star onClick = onToggleFavorite,
IconButton(onClick = onToggleFavorite) { modifier = Modifier.align(Alignment.CenterEnd)
) {
Icon( Icon(
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star, imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites", contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
@@ -273,12 +275,19 @@ private fun ChannelHeader(
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
Row( Box(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(), // Back button - positioned all the way to the left with minimal margin
horizontalArrangement = Arrangement.SpaceBetween, Button(
verticalAlignment = Alignment.CenterVertically onClick = onBackClick,
) { colors = ButtonDefaults.buttonColors(
IconButton(onClick = onBackClick) { 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( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -297,18 +306,21 @@ private fun ChannelHeader(
} }
} }
Spacer(modifier = Modifier.weight(1f)) // Title - perfectly centered regardless of other elements
Text( Text(
text = "channel: $channel", text = "channel: $channel",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = Color(0xFF0080FF), // Blue color = Color(0xFFFF9500), // Orange to match input field
modifier = Modifier.clickable { onSidebarClick() } modifier = Modifier
.align(Alignment.Center)
.clickable { onSidebarClick() }
) )
Spacer(modifier = Modifier.weight(1f)) // Leave button - positioned on the right
TextButton(
TextButton(onClick = onLeaveChannel) { onClick = onLeaveChannel,
modifier = Modifier.align(Alignment.CenterEnd)
) {
Text( Text(
text = "leave", text = "leave",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -139,7 +139,7 @@ private fun appendFormattedContent(
} }
"mention" -> { "mention" -> {
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = Color(0xFFFF8C00), // Orange color = Color(0xFFFF9500), // Orange
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
)) ))
@@ -37,8 +37,9 @@ class CommandProcessor(
when (cmd) { when (cmd) {
"/j", "/join" -> handleJoinCommand(parts, myPeerID) "/j", "/join" -> handleJoinCommand(parts, myPeerID)
"/m", "/msg" -> handleMessageCommand(parts, meshService) "/m", "/msg" -> handleMessageCommand(parts, meshService)
"/w" -> handleWhoCommand() "/w" -> handleWhoCommand(meshService)
"/clear" -> handleClearCommand() "/clear" -> handleClearCommand()
"/pass" -> handlePassCommand(parts, myPeerID)
"/block" -> handleBlockCommand(parts, meshService) "/block" -> handleBlockCommand(parts, meshService)
"/unblock" -> handleUnblockCommand(parts, meshService) "/unblock" -> handleUnblockCommand(parts, meshService)
"/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage) "/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage)
@@ -54,7 +55,8 @@ class CommandProcessor(
if (parts.size > 1) { if (parts.size > 1) {
val channelName = parts[1] val channelName = parts[1]
val channel = if (channelName.startsWith("#")) channelName else "#$channelName" 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) { if (success) {
val systemMessage = BitchatMessage( val systemMessage = BitchatMessage(
sender = "system", sender = "system",
@@ -127,11 +129,11 @@ class CommandProcessor(
} }
} }
private fun handleWhoCommand() { private fun handleWhoCommand(meshService: Any) {
val connectedPeers = state.getConnectedPeersValue() val connectedPeers = state.getConnectedPeersValue()
val peerList = connectedPeers.joinToString(", ") { peerID -> val peerList = connectedPeers.joinToString(", ") { peerID ->
// This would need mesh service access for nicknames // Convert peerID to nickname using the mesh service
peerID // For now just use peer ID getPeerNickname(peerID, meshService)
} }
val systemMessage = BitchatMessage( val systemMessage = BitchatMessage(
@@ -166,6 +168,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) { private fun handleBlockCommand(parts: List<String>, meshService: Any) {
if (parts.size > 1) { if (parts.size > 1) {
val targetName = parts[1].removePrefix("@") val targetName = parts[1].removePrefix("@")
@@ -44,20 +44,17 @@ fun MessageInput(
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Fixed: Remove arrow from private message input // Remove arrow from both private and channel inputs to match DM style
Text( Text(
text = when { text = "<@$nickname>", // No arrow for both private and channel
selectedPrivatePeer != null -> "<@$nickname>" // Removed arrow for private
currentChannel != null -> "<@$nickname> →" // Keep arrow for channels
else -> "<@$nickname>"
},
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium), style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
color = when { color = when {
selectedPrivatePeer != null -> Color(0xFFFF8C00) // Orange for private selectedPrivatePeer != null -> Color(0xFFFF9500) // Orange for private
currentChannel != null -> Color(0xFFFF8C00) // Orange if encrypted channel currentChannel != null -> Color(0xFFFF9500) // Orange for channels too
else -> colorScheme.primary else -> colorScheme.primary
}, },
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace,
fontSize = 14.sp
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
@@ -78,7 +75,7 @@ fun MessageInput(
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing 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( IconButton(
onClick = onSend, onClick = onSend,
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
@@ -87,9 +84,9 @@ fun MessageInput(
modifier = Modifier modifier = Modifier
.size(30.dp) .size(30.dp)
.background( .background(
color = if (selectedPrivatePeer != null) { color = if (selectedPrivatePeer != null || currentChannel != null) {
// Orange for private messages to match nickname color // Orange for both private messages and channels to match nickname color
Color(0xFFFF8C00).copy(alpha = 0.75f) Color(0xFFFF9500).copy(alpha = 0.75f)
} else if (colorScheme.background == Color.Black) { } else if (colorScheme.background == Color.Black) {
Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme
} else { } else {
@@ -100,11 +97,11 @@ fun MessageInput(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = Icons.Filled.KeyboardArrowUp, imageVector = Icons.Filled.KeyboardArrowRight,
contentDescription = "Send message", contentDescription = "Send message",
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
tint = if (selectedPrivatePeer != null) { tint = if (selectedPrivatePeer != null || currentChannel != null) {
// Black arrow on orange in private mode // Black arrow on orange for both private and channel modes
Color.Black Color.Black
} else if (colorScheme.background == Color.Black) { } else if (colorScheme.background == Color.Black) {
Color.Black // Black arrow on bright green in dark theme Color.Black // Black arrow on bright green in dark theme
@@ -193,7 +193,7 @@ fun ChannelsSection(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "#$channel", text = channel, // Channel already contains the # prefix
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = if (isSelected) colorScheme.primary else colorScheme.onSurface, color = if (isSelected) colorScheme.primary else colorScheme.onSurface,
fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal, fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal,
@@ -320,7 +320,7 @@ private fun PeerItem(
imageVector = Icons.Filled.Email, imageVector = Icons.Filled.Email,
contentDescription = "Unread messages", contentDescription = "Unread messages",
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = Color(0xFFFF8C00) // Orange to match private message theme tint = Color(0xFFFF9500) // Orange to match private message theme
) )
} else { } else {
// Signal strength indicators // 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"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </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"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </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()}")
}
}
}
-51
View File
@@ -1,51 +0,0 @@
import androidx.compose.ui.graphics.Color
/**
* 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
*/
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()]
}
fun main() {
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()}")
}
// Test consistency - same user should always get same color
println("\nTesting consistency:")
repeat(3) {
val aliceColor = getUsernameColor("alice")
println("Alice color (test ${it + 1}): ${aliceColor.value.toString(16).uppercase()}")
}
}