mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
Add tap-to-PM and favorite features
- Make sender names tappable in messages to start private chat - Add star button in private chat header to favorite/unfavorite peers - Refactor message view to separate timestamp, sender, and content - Add helper methods for formatting different parts of messages - Own sender name is not tappable (only other users) - Voice notes remain tappable to play/pause
This commit is contained in:
@@ -999,7 +999,11 @@ extension BluetoothMeshService: CBPeripheralDelegate {
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
// Handle write completion if needed
|
||||
if let error = error {
|
||||
print("[PERIPHERAL] Write failed: \(error)")
|
||||
} else {
|
||||
print("[PERIPHERAL] Write completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
|
||||
|
||||
@@ -271,6 +271,97 @@ class ChatViewModel: ObservableObject {
|
||||
return range.location + nickname.count + 2
|
||||
}
|
||||
|
||||
func getSenderColor(for message: BitchatMessage, colorScheme: ColorScheme) -> Color {
|
||||
let isDark = colorScheme == .dark
|
||||
let primaryColor = isDark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
|
||||
if message.sender == nickname {
|
||||
return primaryColor
|
||||
} else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender),
|
||||
let rssi = meshService.getPeerRSSI()[peerID] {
|
||||
return getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme)
|
||||
} else {
|
||||
return primaryColor.opacity(0.9)
|
||||
}
|
||||
}
|
||||
|
||||
func formatVoiceNoteContent(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
let isDark = colorScheme == .dark
|
||||
let primaryColor = isDark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
|
||||
var result = AttributedString()
|
||||
var contentStyle = AttributeContainer()
|
||||
contentStyle.font = .system(size: 14, design: .monospaced)
|
||||
contentStyle.foregroundColor = isDark ? Color.white : Color.black
|
||||
|
||||
// Parse the emoji and duration from content
|
||||
let parts = message.content.components(separatedBy: " ")
|
||||
if parts.count >= 2 {
|
||||
// Emoji
|
||||
result.append(AttributedString(parts[0] + " ").mergingAttributes(contentStyle))
|
||||
|
||||
// Play/pause button as text
|
||||
let playSymbol = audioPlayer.isPlaying && audioPlayer.currentPlayingMessageID == message.id ? "⏸" : "▶"
|
||||
var playStyle = AttributeContainer()
|
||||
playStyle.font = .system(size: 12, design: .monospaced)
|
||||
playStyle.foregroundColor = primaryColor
|
||||
result.append(AttributedString(playSymbol + " ").mergingAttributes(playStyle))
|
||||
|
||||
// Duration
|
||||
result.append(AttributedString(parts[1]).mergingAttributes(contentStyle))
|
||||
} else {
|
||||
result.append(AttributedString(message.content).mergingAttributes(contentStyle))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func formatMessageContent(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
let isDark = colorScheme == .dark
|
||||
let contentText = message.content
|
||||
var processedContent = AttributedString()
|
||||
|
||||
// Regular expression to find @mentions
|
||||
let pattern = "@([a-zA-Z0-9_]+)"
|
||||
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
||||
let matches = regex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: contentText.count)) ?? []
|
||||
|
||||
var lastEndIndex = contentText.startIndex
|
||||
|
||||
for match in matches {
|
||||
// Add text before the mention
|
||||
if let range = Range(match.range(at: 0), in: contentText) {
|
||||
let beforeText = String(contentText[lastEndIndex..<range.lowerBound])
|
||||
if !beforeText.isEmpty {
|
||||
var normalStyle = AttributeContainer()
|
||||
normalStyle.font = .system(size: 14, design: .monospaced)
|
||||
normalStyle.foregroundColor = isDark ? Color.white : Color.black
|
||||
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle))
|
||||
}
|
||||
|
||||
// Add the mention with highlight
|
||||
let mentionText = String(contentText[range])
|
||||
var mentionStyle = AttributeContainer()
|
||||
mentionStyle.font = .system(size: 14, weight: .semibold, design: .monospaced)
|
||||
mentionStyle.foregroundColor = Color.orange
|
||||
processedContent.append(AttributedString(mentionText).mergingAttributes(mentionStyle))
|
||||
|
||||
lastEndIndex = range.upperBound
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text
|
||||
if lastEndIndex < contentText.endIndex {
|
||||
let remainingText = String(contentText[lastEndIndex...])
|
||||
var normalStyle = AttributeContainer()
|
||||
normalStyle.font = .system(size: 14, design: .monospaced)
|
||||
normalStyle.foregroundColor = isDark ? Color.white : Color.black
|
||||
processedContent.append(AttributedString(remainingText).mergingAttributes(normalStyle))
|
||||
}
|
||||
|
||||
return processedContent
|
||||
}
|
||||
|
||||
func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
var result = AttributedString()
|
||||
|
||||
|
||||
@@ -221,14 +221,15 @@ struct ContentView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
// Invisible spacer to balance the back button
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 12))
|
||||
Text("back")
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
// Favorite button
|
||||
Button(action: {
|
||||
viewModel.toggleFavorite(peerID: privatePeerID)
|
||||
}) {
|
||||
Image(systemName: viewModel.isFavorite(peerID: privatePeerID) ? "star.fill" : "star")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(viewModel.isFavorite(peerID: privatePeerID) ? Color.yellow : textColor)
|
||||
}
|
||||
.opacity(0)
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
// Public chat header
|
||||
HStack(spacing: 4) {
|
||||
@@ -359,17 +360,61 @@ struct ContentView: View {
|
||||
// Check if current user is mentioned
|
||||
let isMentioned = message.mentions?.contains(viewModel.nickname) ?? false
|
||||
|
||||
Text(viewModel.formatMessage(message, colorScheme: colorScheme))
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.fontWeight(isMentioned ? .bold : .regular)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.onTapGesture {
|
||||
if message.voiceNoteData != nil {
|
||||
viewModel.playVoiceNote(message: message)
|
||||
if message.sender == "system" {
|
||||
// System messages
|
||||
Text(viewModel.formatMessage(message, colorScheme: colorScheme))
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
// Regular messages with tappable sender name
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
// Timestamp
|
||||
Text("[\\(viewModel.formatTimestamp(message.timestamp))] ")
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundColor(secondaryTextColor)
|
||||
|
||||
// Tappable sender name
|
||||
if message.sender != viewModel.nickname {
|
||||
Button(action: {
|
||||
if let peerID = message.senderPeerID ?? viewModel.getPeerIDForNickname(message.sender) {
|
||||
viewModel.startPrivateChat(with: peerID)
|
||||
}
|
||||
}) {
|
||||
let senderColor = viewModel.getSenderColor(for: message, colorScheme: colorScheme)
|
||||
Text("<\\(message.sender)>")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(senderColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
// Own messages not tappable
|
||||
Text("<\\(message.sender)>")
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
|
||||
Text(" ")
|
||||
|
||||
// Message content
|
||||
if message.voiceNoteData != nil {
|
||||
// Voice note with play button
|
||||
Text(viewModel.formatVoiceNoteContent(message, colorScheme: colorScheme))
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.fontWeight(isMentioned ? .bold : .regular)
|
||||
.onTapGesture {
|
||||
viewModel.playVoiceNote(message: message)
|
||||
}
|
||||
} else {
|
||||
// Regular text content
|
||||
Text(viewModel.formatMessageContent(message, colorScheme: colorScheme))
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.fontWeight(isMentioned ? .bold : .regular)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 2)
|
||||
|
||||
Reference in New Issue
Block a user