From c08fe38c721cbb55cab838bcc64e0aacdc9bffc6 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 24 Jul 2025 12:53:55 +0200 Subject: [PATCH] Fix URL detection and message spacing - Add URL detection to message formatting with blue color and underline - Replace List with ScrollView for precise spacing control - Use minimal vertical padding (1px) between messages - Remove LazyLinkPreviewView, use LinkPreviewView directly - URLs now appear highlighted in message text AND show preview below --- bitchat/ViewModels/ChatViewModel.swift | 10 ++++ bitchat/Views/ContentView.swift | 80 +++++++++----------------- 2 files changed, 36 insertions(+), 54 deletions(-) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index fa805960..d0033f11 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -988,8 +988,12 @@ class ChatViewModel: ObservableObject { let hashtagRegex = try? NSRegularExpression(pattern: hashtagPattern, options: []) let mentionRegex = try? NSRegularExpression(pattern: mentionPattern, options: []) + // Use NSDataDetector for URL detection + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + let hashtagMatches = hashtagRegex?.matches(in: content, options: [], range: NSRange(location: 0, length: content.count)) ?? [] let mentionMatches = mentionRegex?.matches(in: content, options: [], range: NSRange(location: 0, length: content.count)) ?? [] + let urlMatches = detector?.matches(in: content, options: [], range: NSRange(location: 0, length: content.count)) ?? [] // Combine and sort matches var allMatches: [(range: NSRange, type: String)] = [] @@ -999,6 +1003,9 @@ class ChatViewModel: ObservableObject { for match in mentionMatches { allMatches.append((match.range(at: 0), "mention")) } + for match in urlMatches { + allMatches.append((match.range, "url")) + } allMatches.sort { $0.range.location < $1.range.location } // Build content with styling @@ -1029,6 +1036,9 @@ class ChatViewModel: ObservableObject { matchStyle.underlineStyle = .single } else if type == "mention" { matchStyle.foregroundColor = Color.orange + } else if type == "url" { + matchStyle.foregroundColor = Color.blue + matchStyle.underlineStyle = .single } result.append(AttributedString(matchText).mergingAttributes(matchStyle)) diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 204eb6d4..e8bbe0fa 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -163,21 +163,22 @@ struct ContentView: View { private func messagesView(privatePeer: String?) -> some View { ScrollViewReader { proxy in - List { - let messages: [BitchatMessage] = { - if let privatePeer = privatePeer { - let msgs = viewModel.getPrivateChatMessages(for: privatePeer) - return msgs - } else { - return viewModel.messages - } - }() - - // Implement windowing - show last 100 messages for performance - let windowedMessages = messages.suffix(100) - - ForEach(Array(windowedMessages), id: \.id) { message in - VStack(alignment: .leading, spacing: 2) { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + let messages: [BitchatMessage] = { + if let privatePeer = privatePeer { + let msgs = viewModel.getPrivateChatMessages(for: privatePeer) + return msgs + } else { + return viewModel.messages + } + }() + + // Implement windowing - show last 100 messages for performance + let windowedMessages = messages.suffix(100) + + ForEach(Array(windowedMessages), id: \.id) { message in + VStack(alignment: .leading, spacing: 0) { // Check if current user is mentioned let _ = message.mentions?.contains(viewModel.nickname) ?? false @@ -207,12 +208,12 @@ struct ContentView: View { // Check for plain URLs let urls = message.content.extractURLs() - ForEach(Array(urls.prefix(3).enumerated()), id: \.offset) { index, urlInfo in - LazyLinkPreviewView( - url: urlInfo.url, - title: nil, - id: "\(message.id)-\(urlInfo.url.absoluteString)" - ) + if !urls.isEmpty { + ForEach(Array(urls.prefix(3).enumerated()), id: \.offset) { index, urlInfo in + LinkPreviewView(url: urlInfo.url, title: nil) + .padding(.top, 2) + .id("\(message.id)-\(urlInfo.url.absoluteString)") + } } } } @@ -227,13 +228,12 @@ struct ContentView: View { showMessageActions = true } } - .listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)) - .listRowSeparator(.hidden) - .listRowBackground(backgroundColor) + .padding(.horizontal, 12) + .padding(.vertical, 1) } + } + .padding(.vertical, 4) } - .listStyle(.plain) - .scrollContentBackground(.hidden) .background(backgroundColor) .onTapGesture(count: 3) { // Triple-tap to clear current chat @@ -938,34 +938,6 @@ struct MessageContentView: View { } } -// Lazy loading wrapper for LinkPreviewView -struct LazyLinkPreviewView: View { - let url: URL - let title: String? - let id: String - - @State private var shouldLoad = false - - var body: some View { - Group { - if shouldLoad { - LinkPreviewView(url: url, title: title) - .padding(.top, 2) - .id(id) - } - } - .onAppear { - // Delay loading to improve scroll performance - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - shouldLoad = true - } - } - .onDisappear { - shouldLoad = false - } - } -} - // Delivery status indicator view struct DeliveryStatusView: View { let status: DeliveryStatus