Skip to content
Draft
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
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ dependencies {
// Bluetooth
implementation(libs.nordic.ble)

// Giphy
implementation(libs.giphy.ui)

// Coil
implementation(libs.coil.compose)
implementation(libs.coil.gif)

// WebSocket
implementation(libs.okhttp)

Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/bitchat/android/BitchatApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.app.Application
import com.bitchat.android.nostr.RelayDirectory
import com.bitchat.android.ui.theme.ThemePreferenceManager
import com.bitchat.android.net.ArtiTorManager
import com.giphy.sdk.ui.Giphy
import com.bitchat.android.util.AppConstants

/**
* Main application class for bitchat Android
Expand Down Expand Up @@ -53,6 +55,9 @@ class BitchatApplication : Application() {
// Proactively start the foreground service to keep mesh alive
try { com.bitchat.android.service.MeshForegroundService.start(this) } catch (_: Exception) { }

// Initialize GIPHY SDK
Giphy.configure(this, AppConstants.API.GIPHY_API_KEY)

// TorManager already initialized above
}
}
4 changes: 4 additions & 0 deletions app/src/main/java/com/bitchat/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import com.bitchat.android.services.VerificationService
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

import com.bitchat.android.util.AppConstants

class MainActivity : OrientationAwareActivity() {

private lateinit var permissionManager: PermissionManager
Expand Down Expand Up @@ -111,6 +113,8 @@ class MainActivity : OrientationAwareActivity() {

// Initialize permission management
permissionManager = PermissionManager(this)


// Ensure foreground service is running and get mesh instance from holder
try { com.bitchat.android.service.MeshForegroundService.start(applicationContext) } catch (_: Exception) { }
meshService = com.bitchat.android.service.MeshServiceHolder.getOrCreate(applicationContext)
Expand Down
54 changes: 48 additions & 6 deletions app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.ui.media.FullScreenImageViewer
import com.giphy.sdk.ui.views.GiphyDialogFragment
import com.giphy.sdk.ui.themes.GPHTheme
import com.giphy.sdk.ui.GPHContentType
import com.giphy.sdk.ui.GPHSettings
import com.giphy.sdk.core.models.Media
import androidx.appcompat.app.AppCompatActivity

/**
* Main ChatScreen - REFACTORED to use component-based architecture
Expand All @@ -39,6 +45,7 @@ import com.bitchat.android.ui.media.FullScreenImageViewer
*/
@Composable
fun ChatScreen(viewModel: ChatViewModel) {
val context = androidx.compose.ui.platform.LocalContext.current
val colorScheme = MaterialTheme.colorScheme
val messages by viewModel.messages.collectAsStateWithLifecycle()
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
Expand Down Expand Up @@ -101,10 +108,14 @@ fun ChatScreen(viewModel: ChatViewModel) {
}
}

// Determine whether to show media buttons (only hide in geohash location chats)
val showMediaButtons = when {
currentChannel != null -> true
else -> selectedLocationChannel !is com.bitchat.android.geohash.ChannelID.Location
// Determine whether to show media buttons (enabled for all channels)
val showMediaButtons = true
Comment thread
a1denvalu3 marked this conversation as resolved.

// Determine whether to allow binary media (Voice/Images) - restricted to Mesh
val allowBinaryMedia = when {
currentChannel != null -> true // Mesh channels support media
selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location -> false // Nostr/Geohash does not support binary media yet
else -> true // Mesh timeline supports media
}

// Use WindowInsets to handle keyboard properly
Expand Down Expand Up @@ -213,6 +224,32 @@ fun ChatScreen(viewModel: ChatViewModel) {
onSendFileNote = { peer, onionOrChannel, path ->
viewModel.sendFileNote(peer, onionOrChannel, path)
},
onGifClick = {
val activity = context as? AppCompatActivity
if (activity != null) {
val settings = GPHSettings(
theme = if (colorScheme.background == Color.Black) GPHTheme.Dark else GPHTheme.Light,
mediaTypeConfig = arrayOf(GPHContentType.gif, GPHContentType.sticker)
)
val picker = GiphyDialogFragment.newInstance(settings)
picker.gifSelectionListener = object : GiphyDialogFragment.GifSelectionListener {
override fun onGifSelected(media: Media, searchTerm: String?, selectedContentType: GPHContentType) {
val url = media.images.fixedHeight?.gifUrl
if (url != null) {
viewModel.sendMessage(url)
// Force scroll to bottom on next frame
forceScrollToBottom = !forceScrollToBottom
}
picker.dismiss()
}
override fun onDismissed(selectedContentType: GPHContentType) {
// no-op
}
override fun didSearchTerm(term: String) {}
}
picker.show(activity.supportFragmentManager, "giphy_picker")
}
},

showCommandSuggestions = showCommandSuggestions,
commandSuggestions = commandSuggestions,
Expand All @@ -236,7 +273,8 @@ fun ChatScreen(viewModel: ChatViewModel) {
currentChannel = currentChannel,
nickname = nickname,
colorScheme = colorScheme,
showMediaButtons = showMediaButtons
showMediaButtons = showMediaButtons,
allowBinaryMedia = allowBinaryMedia
)
}

Expand Down Expand Up @@ -354,6 +392,7 @@ fun ChatInputSection(
onSendVoiceNote: (String?, String?, String) -> Unit,
onSendImageNote: (String?, String?, String) -> Unit,
onSendFileNote: (String?, String?, String) -> Unit,
onGifClick: () -> Unit,
showCommandSuggestions: Boolean,
commandSuggestions: List<CommandSuggestion>,
showMentionSuggestions: Boolean,
Expand All @@ -364,7 +403,8 @@ fun ChatInputSection(
currentChannel: String?,
nickname: String,
colorScheme: ColorScheme,
showMediaButtons: Boolean
showMediaButtons: Boolean,
allowBinaryMedia: Boolean = true
) {
Surface(
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -397,10 +437,12 @@ fun ChatInputSection(
onSendVoiceNote = onSendVoiceNote,
onSendImageNote = onSendImageNote,
onSendFileNote = onSendFileNote,
onGifClick = onGifClick,
selectedPrivatePeer = selectedPrivatePeer,
currentChannel = currentChannel,
nickname = nickname,
showMediaButtons = showMediaButtons,
allowBinaryMedia = allowBinaryMedia,
modifier = Modifier.fillMaxWidth()
)
}
Expand Down
103 changes: 71 additions & 32 deletions app/src/main/java/com/bitchat/android/ui/InputComponents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,12 @@ fun MessageInput(
onSendVoiceNote: (String?, String?, String) -> Unit,
onSendImageNote: (String?, String?, String) -> Unit,
onSendFileNote: (String?, String?, String) -> Unit,
onGifClick: () -> Unit,
selectedPrivatePeer: String?,
currentChannel: String?,
nickname: String,
showMediaButtons: Boolean,
allowBinaryMedia: Boolean = true,
modifier: Modifier = Modifier
) {
val colorScheme = MaterialTheme.colorScheme
Expand Down Expand Up @@ -273,46 +275,83 @@ fun MessageInput(
// onSendFileNote(latestSelectedPeer.value, latestChannel.value, path)
// }
//)
ImagePickerButton(
onImageReady = { outPath ->
onSendImageNote(latestSelectedPeer.value, latestChannel.value, outPath)
if (allowBinaryMedia) {
ImagePickerButton(
onImageReady = { outPath ->
onSendImageNote(latestSelectedPeer.value, latestChannel.value, outPath)
}
)
}

Spacer(Modifier.width(1.dp))

// GIF Button
IconButton(
onClick = onGifClick,
modifier = Modifier.size(32.dp)
) {
Box(
modifier = Modifier
.size(30.dp)
.background(
color = if (colorScheme.background == Color.Black) Color(0xFF333333) else Color(0xFFE0E0E0),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = "GIF",
style = MaterialTheme.typography.bodySmall.copy(
fontWeight = FontWeight.Bold,
fontSize = 10.sp
),
color = colorScheme.primary
)
}
)
}
}
}

Spacer(Modifier.width(1.dp))

VoiceRecordButton(
backgroundColor = bg,
onStart = {
isRecording = true
elapsedMs = 0L
// Keep existing focus to avoid IME collapse, but do not force-show keyboard
if (isFocused.value) {
try { focusRequester.requestFocus() } catch (_: Exception) {}
}
},
onAmplitude = { amp, ms ->
amplitude = amp
elapsedMs = ms
},
onFinish = { path ->
isRecording = false
// Extract and cache waveform from the actual audio file to match receiver rendering
AudioWaveformExtractor.extractAsync(path, sampleCount = 120) { arr ->
if (arr != null) {
try { com.bitchat.android.features.voice.VoiceWaveformCache.put(path, arr) } catch (_: Exception) {}
if (allowBinaryMedia) {
VoiceRecordButton(
backgroundColor = bg,
onStart = {
isRecording = true
elapsedMs = 0L
// Keep existing focus to avoid IME collapse, but do not force-show keyboard
if (isFocused.value) {
try {
focusRequester.requestFocus()
} catch (_: Exception) {
}
}
},
onAmplitude = { amp, ms ->
amplitude = amp
elapsedMs = ms
},
onFinish = { path ->
isRecording = false
// Extract and cache waveform from the actual audio file to match receiver rendering
AudioWaveformExtractor.extractAsync(path, sampleCount = 120) { arr ->
if (arr != null) {
try {
com.bitchat.android.features.voice.VoiceWaveformCache.put(path, arr)
} catch (_: Exception) {
}
}
}
// BLE path (private or public) — use latest values to avoid stale captures
latestOnSendVoiceNote.value(
latestSelectedPeer.value,
latestChannel.value,
path
)
}
// BLE path (private or public) — use latest values to avoid stale captures
latestOnSendVoiceNote.value(
latestSelectedPeer.value,
latestChannel.value,
path
)
}
)
)
}

} else {
// Send button with enabled/disabled state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ fun PrivateChatSheet(
onSendFileNote = { peer, channel, path ->
viewModel.sendFileNote(peer, channel, path)
},
onGifClick = { },
showCommandSuggestions = false,
commandSuggestions = emptyList(),
showMentionSuggestions = false,
Expand All @@ -904,7 +905,8 @@ fun PrivateChatSheet(
currentChannel = null,
nickname = nickname,
colorScheme = colorScheme,
showMediaButtons = true
showMediaButtons = true,
allowBinaryMedia = !isNostrPeer
)
}

Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/com/bitchat/android/ui/MessageComponents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.shape.CircleShape
import com.bitchat.android.ui.media.FileMessageItem
import com.bitchat.android.ui.media.GiphyMessageItem
import com.bitchat.android.model.BitchatMessageType
import com.bitchat.android.R
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -244,6 +245,23 @@ fun MessageItem(
return
}

// GIPHY special rendering - robust check for various GIPHY URL formats
if (message.type == BitchatMessageType.Message &&
(message.content.contains("giphy.com/media") || message.content.contains("media.giphy.com")) &&
(message.content.contains(".gif") || message.content.contains(".webp"))) {
GiphyMessageItem(
message = message,
currentUserNickname = currentUserNickname,
meshService = meshService,
colorScheme = colorScheme,
timeFormatter = timeFormatter,
onNicknameClick = onNicknameClick,
onMessageLongPress = onMessageLongPress,
modifier = modifier
)
return
}

// File special rendering
if (message.type == BitchatMessageType.File) {
val path = message.content.trim()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package com.bitchat.android.ui

import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import com.bitchat.android.utils.DeviceUtils

/**
* Base activity that automatically sets orientation based on device type.
* Tablets can rotate to landscape, phones are locked to portrait.
*/
abstract class OrientationAwareActivity : ComponentActivity() {
abstract class OrientationAwareActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
Loading
Loading