Media transfers (#440)

* tor voice wip

* worky BLE a bit

* can send sound

* remove tor

* ui cleanup

* recording time

* progress bar color

* nicknames for audio

* onboarding permissions no microphone

* may work

* fix destionation

* extend

* refactor voice input component

* fix keyboard collapse issue

* send images

* wip image open

* image sending works

* wip waveforms

* better

* better animation

* fix cursor for sending audio

* image sending animation

* image sending animation

* full screen image viewer

* gossip sync for fragments too

* reduce delays

* fix keyboard focus

* use v2 for file transfers

* do not sync fragments

* scrollable image viewer

* ui

* ui adjustments

* nicer animation

* seek through audio

* add spec

* add more details to documentation

* File sharing E2E:
- Add TLV BitchatFilePacket, FileSharingManager
- Implement sendFileNote in ChatViewModel
- File receive path: save to files/incoming and render [file] messages with FileMessageItem or FileSendingAnimation during transfer
- SAF FilePickerButton and dispatcher wiring; image/file choice to follow in MediaPickerOptions
- Add FileViewerDialog with system open/save, FileProvider and file_paths
- Hook transfer progress to file sending UI
- Manifest: READ_MEDIA_* and FileProvider
- Fix MessageHandler saving and prefix for non-image payloads
- Add helper utils (FileUtils)

* kinda wip

* fix buttons

* files half working

* wip file transfer

* file packet has 2-byte TLV and chunks. it wokrs but it sucks

* clean

* remove gossip sync for fragments

* fix audio and image rendering

* adjust FILE_SIZE TLV size too

* cleanup

* haptic

* private messages media

* read receipts for media

* use enum for message type not string

* delivery ack checks dont push content

* check

* animation fix

* refactor

* ui adjustments

* comments

* refactor

* fix crash on send and receive of the same file

* refactor notifications

* tests
This commit is contained in:
callebtc
2025-09-19 22:46:14 +02:00
committed by GitHub
parent 1178fc254a
commit 633a506753
57 changed files with 4801 additions and 138 deletions
@@ -156,6 +156,105 @@ fun formatMessageAsAnnotatedString(
return builder.toAnnotatedString()
}
/**
* Build only the nickname + timestamp header line for a message, matching styles of normal messages.
*/
fun formatMessageHeaderAnnotatedString(
message: BitchatMessage,
currentUserNickname: String,
meshService: BluetoothMeshService,
colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
): AnnotatedString {
val builder = AnnotatedString.Builder()
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
val isSelf = message.senderPeerID == meshService.myPeerID ||
message.sender == currentUserNickname ||
message.sender.startsWith("$currentUserNickname#")
if (message.sender != "system") {
val baseColor = if (isSelf) Color(0xFFFF9500) else getPeerColor(message, isDark)
val (baseName, suffix) = splitSuffix(message.sender)
// "<@"
builder.pushStyle(SpanStyle(
color = baseColor,
fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
))
builder.append("<@")
builder.pop()
// Base name (clickable when not self)
builder.pushStyle(SpanStyle(
color = baseColor,
fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
))
val nicknameStart = builder.length
builder.append(truncateNickname(baseName))
val nicknameEnd = builder.length
if (!isSelf) {
builder.addStringAnnotation(
tag = "nickname_click",
annotation = (message.originalSender ?: message.sender),
start = nicknameStart,
end = nicknameEnd
)
}
builder.pop()
// Hashtag suffix
if (suffix.isNotEmpty()) {
builder.pushStyle(SpanStyle(
color = baseColor.copy(alpha = 0.6f),
fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
))
builder.append(suffix)
builder.pop()
}
// Sender suffix ">"
builder.pushStyle(SpanStyle(
color = baseColor,
fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
))
builder.append(">")
builder.pop()
// Timestamp and optional PoW bits, matching normal message appearance
builder.pushStyle(SpanStyle(
color = Color.Gray.copy(alpha = 0.7f),
fontSize = (BASE_FONT_SIZE - 4).sp
))
builder.append(" [${timeFormatter.format(message.timestamp)}]")
message.powDifficulty?.let { bits ->
if (bits > 0) builder.append("${bits}b")
}
builder.pop()
} else {
// System message header (should rarely apply to voice)
builder.pushStyle(SpanStyle(
color = Color.Gray,
fontSize = (BASE_FONT_SIZE - 2).sp,
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
))
builder.append("* ${message.content} *")
builder.pop()
builder.pushStyle(SpanStyle(
color = Color.Gray.copy(alpha = 0.5f),
fontSize = (BASE_FONT_SIZE - 4).sp
))
builder.append(" [${timeFormatter.format(message.timestamp)}]")
builder.pop()
}
return builder.toAnnotatedString()
}
/**
* iOS-style peer color assignment using djb2 hash algorithm
* Avoids orange (~30°) reserved for self messages