Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ class BluetoothMeshService(private val context: Context) {
override fun onReadReceiptReceived(receipt: ReadReceipt) {
delegate?.didReceiveReadReceipt(receipt)
}

override fun markChannelAsPasswordProtected(channel: String) {
delegate?.markChannelAsPasswordProtected(channel)
}
}

// PacketProcessor delegates
Expand Down Expand Up @@ -597,5 +601,6 @@ interface BluetoothMeshDelegate {
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
fun getNickname(): String?
fun isFavorite(peerID: String): Boolean
fun markChannelAsPasswordProtected(channel: String)
fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray)
}
6 changes: 6 additions & 0 deletions app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ class MessageHandler(private val myPeerID: String) {
timestamp = Date() // Use current time instead of original timestamp
)

// Auto-detect password-protected channels
if (message.channel != null && message.isEncrypted && message.encryptedContent != null) {
delegate?.markChannelAsPasswordProtected(message.channel)
}

delegate?.onMessageReceived(messageWithCurrentTime)
}

Expand Down Expand Up @@ -354,6 +359,7 @@ interface MessageHandlerDelegate {

// Callbacks
fun onMessageReceived(message: BitchatMessage)
fun markChannelAsPasswordProtected(channel: String)
fun onChannelLeave(channel: String, fromPeer: String)
fun onPeerDisconnected(nickname: String)
fun onDeliveryAckReceived(ack: DeliveryAck)
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/java/com/bitchat/android/ui/ChannelManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,27 @@ class ChannelManager(
)
}

fun markChannelAsPasswordProtected(channel: String) {
// Mark as password protected if not already
if (!state.getPasswordProtectedChannelsValue().contains(channel)) {
state.setPasswordProtectedChannels(
state.getPasswordProtectedChannelsValue().plus(channel)
)
}

// Track as discovered if not joined
if (!state.getJoinedChannelsValue().contains(channel)) {
state.setDiscoveredChannels(
state.getDiscoveredChannelsValue().plus(channel)
)
}

// Save state
saveChannelData()
}

fun getDiscoveredChannels(): Set<String> = state.getDiscoveredChannelsValue()

// MARK: - Emergency Clear

fun clearAllChannels() {
Expand Down
25 changes: 10 additions & 15 deletions app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,11 @@ fun ChatScreen(viewModel: ChatViewModel) {
val showAppInfo by viewModel.showAppInfo.observeAsState(false)

var messageText by remember { mutableStateOf(TextFieldValue("")) }
var showPasswordPrompt by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) }
val showPasswordPrompt by viewModel.showPasswordPrompt.observeAsState(false)
val passwordPromptChannel by viewModel.passwordPromptChannel.observeAsState(null)
var passwordInput by remember { mutableStateOf("") }

// Show password dialog when needed
LaunchedEffect(showPasswordPrompt) {
showPasswordDialog = showPasswordPrompt
}

val isConnected by viewModel.isConnected.observeAsState(false)
val passwordPromptChannel by viewModel.passwordPromptChannel.observeAsState(null)

// Determine what messages to show
val displayMessages = when {
Expand Down Expand Up @@ -173,21 +167,22 @@ fun ChatScreen(viewModel: ChatViewModel) {

// Dialogs
ChatDialogs(
showPasswordDialog = showPasswordDialog,
showPasswordPrompt = showPasswordPrompt,
passwordPromptChannel = passwordPromptChannel,
passwordInput = passwordInput,
onPasswordChange = { passwordInput = it },
onPasswordConfirm = {
if (passwordInput.isNotEmpty()) {
val success = viewModel.joinChannel(passwordPromptChannel!!, passwordInput)
val channel = passwordPromptChannel
if (passwordInput.isNotEmpty() && channel != null) {
val success = viewModel.joinChannel(channel, passwordInput)
if (success) {
showPasswordDialog = false
viewModel.hidePasswordPrompt()
passwordInput = ""
}
}
},
onPasswordDismiss = {
showPasswordDialog = false
viewModel.hidePasswordPrompt()
passwordInput = ""
},
showAppInfo = showAppInfo,
Expand Down Expand Up @@ -298,7 +293,7 @@ private fun ChatFloatingHeader(

@Composable
private fun ChatDialogs(
showPasswordDialog: Boolean,
showPasswordPrompt: Boolean,
passwordPromptChannel: String?,
passwordInput: String,
onPasswordChange: (String) -> Unit,
Expand All @@ -309,7 +304,7 @@ private fun ChatDialogs(
) {
// Password dialog
PasswordPromptDialog(
show = showPasswordDialog,
show = showPasswordPrompt,
channelName = passwordPromptChannel,
passwordInput = passwordInput,
onPasswordChange = onPasswordChange,
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/bitchat/android/ui/ChatState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class ChatState {
private val _passwordProtectedChannels = MutableLiveData<Set<String>>(emptySet())
val passwordProtectedChannels: LiveData<Set<String>> = _passwordProtectedChannels

private val _discoveredChannels = MutableLiveData<Set<String>>(emptySet())
val discoveredChannels: LiveData<Set<String>> = _discoveredChannels

private val _showPasswordPrompt = MutableLiveData<Boolean>(false)
val showPasswordPrompt: LiveData<Boolean> = _showPasswordPrompt

Expand Down Expand Up @@ -116,6 +119,7 @@ class ChatState {
fun getChannelMessagesValue() = _channelMessages.value ?: emptyMap()
fun getUnreadChannelMessagesValue() = _unreadChannelMessages.value ?: emptyMap()
fun getPasswordProtectedChannelsValue() = _passwordProtectedChannels.value ?: emptySet()
fun getDiscoveredChannelsValue() = _discoveredChannels.value ?: emptySet()
fun getShowPasswordPromptValue() = _showPasswordPrompt.value ?: false
fun getPasswordPromptChannelValue() = _passwordPromptChannel.value
fun getShowSidebarValue() = _showSidebar.value ?: false
Expand Down Expand Up @@ -173,6 +177,10 @@ class ChatState {
_passwordProtectedChannels.value = channels
}

fun setDiscoveredChannels(channels: Set<String>) {
_discoveredChannels.value = channels
}

fun setShowPasswordPrompt(show: Boolean) {
_showPasswordPrompt.value = show
}
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class ChatViewModel(
val channelMessages: LiveData<Map<String, List<BitchatMessage>>> = state.channelMessages
val unreadChannelMessages: LiveData<Map<String, Int>> = state.unreadChannelMessages
val passwordProtectedChannels: LiveData<Set<String>> = state.passwordProtectedChannels
val discoveredChannels: LiveData<Set<String>> = state.discoveredChannels
val showPasswordPrompt: LiveData<Boolean> = state.showPasswordPrompt
val passwordPromptChannel: LiveData<String?> = state.passwordPromptChannel
val showSidebar: LiveData<Boolean> = state.showSidebar
Expand Down Expand Up @@ -149,6 +150,24 @@ class ChatViewModel(
meshService.sendMessage("left $channel")
}

fun onChannelClick(channel: String) {
if (state.getJoinedChannelsValue().contains(channel)) {
// Already joined, just switch
switchToChannel(channel)
} else if (state.getPasswordProtectedChannelsValue().contains(channel)) {
// Need password
state.setPasswordPromptChannel(channel)
state.setShowPasswordPrompt(true)
} else {
// Regular join
joinChannel(channel, null)
}
}

fun hidePasswordPrompt() {
channelManager.hidePasswordPrompt()
}

// MARK: - Private Chat Management (delegated)

fun startPrivateChat(peerID: String) {
Expand Down Expand Up @@ -348,6 +367,10 @@ class ChatViewModel(
privateChatManager.registerPeerPublicKey(peerID, publicKeyData)
}

override fun markChannelAsPasswordProtected(channel: String) {
meshDelegateHandler.markChannelAsPasswordProtected(channel)
}

// MARK: - Emergency Clear

fun panicClearAllData() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,10 @@ class MeshDelegateHandler(
override fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) {
privateChatManager.registerPeerPublicKey(peerID, publicKeyData)
}

override fun markChannelAsPasswordProtected(channel: String) {
coroutineScope.launch {
channelManager.markChannelAsPasswordProtected(channel)
}
}
}
95 changes: 92 additions & 3 deletions app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ fun SidebarOverlay(
val colorScheme = MaterialTheme.colorScheme
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
val joinedChannels by viewModel.joinedChannels.observeAsState(emptyList())
val discoveredChannels by viewModel.discoveredChannels.observeAsState(emptySet())
val passwordProtectedChannels by viewModel.passwordProtectedChannels.observeAsState(emptySet())
val currentChannel by viewModel.currentChannel.observeAsState()
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.observeAsState()
val nickname by viewModel.nickname.observeAsState("")
Expand Down Expand Up @@ -96,10 +98,30 @@ fun SidebarOverlay(
onLeaveChannel = { channel ->
viewModel.leaveChannel(channel)
},
unreadChannelMessages = unreadChannelMessages
unreadChannelMessages = unreadChannelMessages,
passwordProtectedChannels = passwordProtectedChannels
)
}

}

// Discovered channels section
val notJoinedDiscoveredChannels = discoveredChannels.filter { !joinedChannels.contains(it) }
if (notJoinedDiscoveredChannels.isNotEmpty()) {
item {
DiscoveredChannelsSection(
channels = notJoinedDiscoveredChannels.toList(),
passwordProtectedChannels = passwordProtectedChannels,
colorScheme = colorScheme,
onChannelClick = { channel ->
viewModel.onChannelClick(channel)
onDismiss()
}
)
}
}

// Divider between channels and people
if (joinedChannels.isNotEmpty() || notJoinedDiscoveredChannels.isNotEmpty()) {
item {
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
}
Expand Down Expand Up @@ -158,7 +180,8 @@ fun ChannelsSection(
colorScheme: ColorScheme,
onChannelClick: (String) -> Unit,
onLeaveChannel: (String) -> Unit,
unreadChannelMessages: Map<String, Int> = emptyMap()
unreadChannelMessages: Map<String, Int> = emptyMap(),
passwordProtectedChannels: Set<String> = emptySet()
) {
Column {
Row(
Expand Down Expand Up @@ -212,6 +235,15 @@ fun ChannelsSection(
modifier = Modifier.weight(1f)
)

// Lock icon for password-protected channels
if (passwordProtectedChannels.contains(channel)) {
Text(
text = "🔒",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 4.dp)
)
}

// Leave channel button
IconButton(
onClick = { onLeaveChannel(channel) },
Expand All @@ -229,6 +261,63 @@ fun ChannelsSection(
}
}

@Composable
fun DiscoveredChannelsSection(
channels: List<String>,
passwordProtectedChannels: Set<String>,
colorScheme: ColorScheme,
onChannelClick: (String) -> Unit
) {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Person, // Using Person icon as placeholder
contentDescription = null,
modifier = Modifier.size(10.dp),
tint = colorScheme.onSurface.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = stringResource(id = R.string.discovered_channels).uppercase(),
style = MaterialTheme.typography.labelSmall,
color = colorScheme.onSurface.copy(alpha = 0.6f),
fontWeight = FontWeight.Bold
)
}

channels.forEach { channel ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onChannelClick(channel) }
.padding(horizontal = 24.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = channel, // Channel already contains the # prefix
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.weight(1f)
)

// Lock icon for password-protected channels
if (passwordProtectedChannels.contains(channel)) {
Text(
text = "🔒",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
}
}
}

@Composable
fun PeopleSection(
connectedPeers: List<String>,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<string name="back">Back</string>
<string name="people">People</string>
<string name="channels">Channels</string>
<string name="discovered_channels">Discovered Channels</string>
<string name="online_users">Online Users</string>
<string name="no_one_connected">No one connected</string>
<string name="emergency_clear_hint">Triple tap to clear all data</string>
Expand Down
Loading