diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index f8c05b24..821ddd9d 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -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]) { diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 5b8c0416..6bf521af 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -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.. AttributedString { var result = AttributedString() diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 6b6464b3..efd366c7 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -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)