aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-10-25 15:26:05 -0700
committerDustin Mierau <dustin@mierau.me>2025-10-25 15:26:05 -0700
commit53686e30592fee566585e391738adee8b2ef2137 (patch)
tree7ee768ca3c84ac7dad7ea674fb637480e4b7184c /Hotline
parent65521893ac5f10ec035cd735747034c263e46d77 (diff)
Fixed some warnings. Add basic markdown formatting support to chat messages. Add some metadata support for chat log (though we're not using this yet).
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Hotline/HotlineProtocol.swift4
-rw-r--r--Hotline/Managers/ChatStore.swift26
-rw-r--r--Hotline/Models/ChatMessage.swift9
-rw-r--r--Hotline/Models/Hotline.swift8
-rw-r--r--Hotline/Utility/FoundationExtensions.swift34
-rw-r--r--Hotline/Utility/SwiftUIExtensions.swift26
-rw-r--r--Hotline/macOS/ChatView.swift49
-rw-r--r--Hotline/macOS/FilesView.swift3
8 files changed, 98 insertions, 61 deletions
diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift
index ff91672..9b3a812 100644
--- a/Hotline/Hotline/HotlineProtocol.swift
+++ b/Hotline/Hotline/HotlineProtocol.swift
@@ -241,11 +241,11 @@ struct HotlineAccount: Identifiable {
}
if fieldType == .userLogin {
- self.login = field.getObfuscatedString()!
+ self.login = field.getObfuscatedString() ?? ""
}
if fieldType == .userPassword {
- self.password = field.getObfuscatedString()!
+ self.password = field.getObfuscatedString() ?? ""
}
if fieldType == .userAccess, let opts = field.getUInt64(){
diff --git a/Hotline/Managers/ChatStore.swift b/Hotline/Managers/ChatStore.swift
index daf8ca0..59c9484 100644
--- a/Hotline/Managers/ChatStore.swift
+++ b/Hotline/Managers/ChatStore.swift
@@ -28,12 +28,23 @@ actor ChatStore {
}
}
+ struct EntryMetadata: Codable {
+ var images: [ImageMetadata]?
+
+ struct ImageMetadata: Codable {
+ let url: String
+ let width: CGFloat?
+ let height: CGFloat?
+ }
+ }
+
struct Entry: Codable {
let id: UUID
let body: String
let username: String?
let type: String
let date: Date
+ var metadata: EntryMetadata?
}
struct LoadResult {
@@ -81,6 +92,21 @@ actor ChatStore {
}
}
+ func updateMetadata(_ metadata: EntryMetadata, for entryID: UUID, key: SessionKey) async {
+ do {
+ guard var logFile = try loadLogFile(for: key) else { return }
+
+ if let index = logFile.entries.firstIndex(where: { $0.id == entryID }) {
+ logFile.entries[index].metadata = metadata
+ cache[key] = logFile
+ try persist(logFile, for: key)
+ }
+ }
+ catch {
+ print("ChatStore: failed to update metadata —", error)
+ }
+ }
+
func loadHistory(for key: SessionKey, limit: Int? = nil) async -> LoadResult {
do {
let logFile = try loadLogFile(for: key)
diff --git a/Hotline/Models/ChatMessage.swift b/Hotline/Models/ChatMessage.swift
index 744a5d2..1c210ce 100644
--- a/Hotline/Models/ChatMessage.swift
+++ b/Hotline/Models/ChatMessage.swift
@@ -48,19 +48,22 @@ extension ChatMessageType {
}
struct ChatMessage: Identifiable {
- let id = UUID()
-
+ let id: UUID
+
let text: String
let type: ChatMessageType
let date: Date
let username: String?
+ var metadata: ChatStore.EntryMetadata?
static let parser = /^\s*([^\:]+):\s*([\s\S]+)$/
init(text: String, type: ChatMessageType, date: Date) {
+ self.id = UUID()
self.type = type
self.date = date
-
+ self.metadata = nil
+
if
type == .message,
let match = text.firstMatch(of: ChatMessage.parser) {
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index 08b79a1..3783ba0 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -1445,7 +1445,9 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
else {
renderedText = entry.body
}
- return ChatMessage(text: renderedText, type: chatType, date: entry.date)
+ var message = ChatMessage(text: renderedText, type: chatType, date: entry.date)
+ message.metadata = entry.metadata
+ return message
}
self.chat = historyMessages + currentMessages
self.lastPersistedMessageType = historyMessages.last?.type
@@ -1530,12 +1532,12 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
guard let parent = self.findNews(in: self.news, at: path), !parent.children.isEmpty else {
return nil
}
-
+
return parent.children.first { child in
guard let childArticleID = child.articleID else {
return false
}
-
+
return child.type == .article && child.articleID == childArticleID
}
}
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index 9fc9ae7..90a3032 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -8,6 +8,13 @@ enum Endianness {
extension String {
+ func markdownToAttributedString() -> AttributedString {
+ let markdownText = self.convertingLinksToMarkdown()
+ let attr = (try? AttributedString(markdown: markdownText, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace))) ?? AttributedString(self)
+
+ return attr
+ }
+
func convertToAttributedStringWithLinks() -> AttributedString {
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self)
let matches = self.ranges(of: RegularExpressions.relaxedLink)
@@ -54,17 +61,28 @@ extension String {
}
func convertingLinksToMarkdown() -> String {
- var cp = String(self)
- cp.replace(RegularExpressions.relaxedLink) { match -> String in
+// var cp = String(self)
+
+ self.replacing(RegularExpressions.relaxedLink) { match in
let linkText = self[match.range]
- var injectedScheme = "https://"
- if let _ = try? RegularExpressions.supportedLinkScheme.prefixMatch(in: linkText) {
- injectedScheme = ""
- }
- return "[\(linkText)](\(injectedScheme)\(linkText))"
+ // Only add https:// if the link doesn't already have a scheme
+ let hasScheme = (try? RegularExpressions.supportedLinkScheme.prefixMatch(in: linkText)) != nil
+ let url = hasScheme ? String(linkText) : "https://\(linkText)"
+
+ return "[\(linkText)](\(url))"
}
- return cp
+
+// cp.replace(RegularExpressions.relaxedLink) { match -> String in
+// let linkText = self[match.range]
+// var injectedScheme = "https://"
+// if let _ = try? RegularExpressions.supportedLinkScheme.prefixMatch(in: linkText) {
+// injectedScheme = ""
+// }
+//
+// return "[\(linkText)](\(injectedScheme)\(linkText))"
+// }
+// return cp
}
}
diff --git a/Hotline/Utility/SwiftUIExtensions.swift b/Hotline/Utility/SwiftUIExtensions.swift
index 3b850e0..12217b2 100644
--- a/Hotline/Utility/SwiftUIExtensions.swift
+++ b/Hotline/Utility/SwiftUIExtensions.swift
@@ -1,7 +1,33 @@
import SwiftUI
+import Foundation
extension Color {
init(hex: Int, opacity: Double = 1.0) {
self.init(red: Double((hex >> 16) & 0xFF) / 255.0, green: Double((hex >> 8) & 0xFF) / 255.0, blue: Double(hex & 0xFF) / 255.0, opacity: opacity)
}
}
+
+extension AttributedString {
+ func setHangingIndent(firstLineHeadIndent: CGFloat = 0, otherLinesHeadIndent: CGFloat) -> AttributedString {
+// var blah = self
+
+// guard var paragraph = self.paragraphStyle else {
+// return
+// }
+
+ var p = self.paragraphStyle?.mutableCopy() as? NSMutableParagraphStyle
+ p?.headIndent = otherLinesHeadIndent
+ p?.firstLineHeadIndent = firstLineHeadIndent
+
+// paragraph.headIndent = otherLinesHeadIndent // indent for lines 2+
+// paragraph.firstLineHeadIndent = firstLineHeadIndent // usually 0
+
+ var blah = self
+
+
+ blah.paragraphStyle = p
+
+ return blah
+ }
+}
+
diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift
index a45e7ed..2ad9816 100644
--- a/Hotline/macOS/ChatView.swift
+++ b/Hotline/macOS/ChatView.swift
@@ -69,58 +69,21 @@ struct ChatDisconnectedMessageView: View {
struct ChatMessageView: View {
let message: ChatMessage
-
+
var body: some View {
HStack(alignment: .firstTextBaseline) {
if let username = message.username {
- // if msg.text.isImageURL() {
- // HStack(alignment: .bottom) {
- // Text("**\(username):** ")
- //
- // let imageURL = URL(string: msg.text)!
- // AsyncImage(url: imageURL) { phase in
- // switch phase {
- // case .failure:
- // Text(LocalizedStringKey(msg.text.convertLinksToMarkdown()))
- // .lineSpacing(4)
- // .multilineTextAlignment(.leading)
- // .textSelection(.enabled)
- // .tint(Color("Link Color"))
- // case .success(let img):
- // Link(destination: imageURL) {
- // img
- // .resizable()
- // .scaledToFit()
- // .frame(maxWidth: 250, maxHeight: 150, alignment: .leading)
- // .onAppear {
- // reader.scrollTo(bottomID, anchor: .bottom)
- // }
- // }
- // default:
- // ProgressView().controlSize(.small)
- // }
- // }
- //
- // Spacer()
- // }
- // }
- // else {
- Text(LocalizedStringKey("**\(username):** \(message.text)".convertingLinksToMarkdown()))
- .lineSpacing(4)
- .multilineTextAlignment(.leading)
- .textSelection(.enabled)
- .tint(Color("Link Color"))
- // }
+ Text("\(username): ").fontWeight(.semibold) + Text(message.text.markdownToAttributedString())
}
else {
Text(message.text)
- .lineSpacing(4)
- .multilineTextAlignment(.leading)
- .textSelection(.enabled)
- .tint(Color("Link Color"))
}
Spacer()
}
+ .lineSpacing(4)
+ .multilineTextAlignment(.leading)
+ .textSelection(.enabled)
+ .tint(Color("Link Color"))
}
}
diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift
index 00a4e1e..42232af 100644
--- a/Hotline/macOS/FilesView.swift
+++ b/Hotline/macOS/FilesView.swift
@@ -244,11 +244,10 @@ struct FilesView: View {
return "No files found in \(processed) \(folderWord)"
}
return "\(count) file\(count == 1 ? "" : "s") found in \(processed) \(folderWord)"
- case .cancelled(let processed):
+ case .cancelled(_):
if model.fileSearchResults.isEmpty {
return nil
}
- let folderWord = processed == 1 ? "folder" : "folders"
return "Search cancelled"
case .failed(let message):
return "Search failed: \(message)"