aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline/Hotline/HotlineClient.swift14
-rw-r--r--Hotline/Hotline/HotlineExtensions.swift94
-rw-r--r--Hotline/Hotline/Transfers/HotlineFileUploadClient.swift4
-rw-r--r--Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift5
-rw-r--r--Hotline/Hotline/Transfers/HotlineTransferClient.swift6
-rw-r--r--Hotline/MacApp.swift4
-rw-r--r--Hotline/State/HotlineState.swift50
-rw-r--r--Hotline/macOS/Board/MessageBoardView.swift20
-rw-r--r--Hotline/macOS/Files/FolderItemView.swift2
-rw-r--r--Hotline/macOS/MessageView.swift8
-rw-r--r--Hotline/macOS/ServerView.swift33
11 files changed, 109 insertions, 131 deletions
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift
index 29b7827..2219330 100644
--- a/Hotline/Hotline/HotlineClient.swift
+++ b/Hotline/Hotline/HotlineClient.swift
@@ -82,10 +82,10 @@ public struct HotlineLogin: Sendable {
/// Information about the connected server
public struct HotlineServerInfo: Sendable {
- let name: String
+ let name: String?
let version: UInt16
- public init(name: String, version: UInt16) {
+ public init(name: String?, version: UInt16) {
self.name = name
self.version = version
}
@@ -242,7 +242,7 @@ public actor HotlineClient {
print("HotlineClient.connect(): Starting keep-alive")
await client.startKeepAlive()
- print("HotlineClient.connect(): Connected to \(serverInfo.name) (v\(serverInfo.version))")
+ print("HotlineClient.connect(): Connected to \(serverInfo.name ?? "Server") (v\(serverInfo.version))")
return client
}
@@ -279,8 +279,12 @@ public actor HotlineClient {
throw HotlineClientError.loginFailed(errorText)
}
- let serverName = reply.getField(type: .serverName)?.getString() ?? "Unknown"
- let serverVersion = reply.getField(type: .versionNumber)?.getUInt16() ?? 0
+ // All servers send a server version.
+ let serverVersion = reply.getField(type: .versionNumber)?.getUInt16() ?? 123
+
+ // Later clients send a name and banner ID.
+ let serverName = reply.getField(type: .serverName)?.getString()
+// let serverBannerID = reply.getField(type: .communityBannerID)?.getInteger()
return HotlineServerInfo(name: serverName, version: serverVersion)
}
diff --git a/Hotline/Hotline/HotlineExtensions.swift b/Hotline/Hotline/HotlineExtensions.swift
index bcee812..0d5d806 100644
--- a/Hotline/Hotline/HotlineExtensions.swift
+++ b/Hotline/Hotline/HotlineExtensions.swift
@@ -490,7 +490,7 @@ extension FileManager {
return nil
}
- guard let fileName = fileURL.lastPathComponent.data(using: .macOSRoman) else {
+ guard let fileName = fileURL.lastPathComponent.data(using: .macOSRoman, allowLossyConversion: true) else {
return nil
}
@@ -826,38 +826,11 @@ extension Array where Element == UInt8 {
}
func readString(at offset: Int, length: Int) -> String? {
- guard let subdata: Data = self.readData(at: offset, length: length) else {
+ guard let data: Data = self.readData(at: offset, length: length) else {
return nil
}
- if subdata.count == 0 {
- return ""
- }
-
- let allowedEncodings = [
- NSUTF8StringEncoding,
- NSShiftJISStringEncoding,
- NSUnicodeStringEncoding,
- NSWindowsCP1251StringEncoding
- ]
-
- var decodedNSString: NSString?
- let rawValue = NSString.stringEncoding(for: subdata, encodingOptions: [.allowLossyKey: false], convertedString: &decodedNSString, usedLossyConversion: nil)
-
- if allowedEncodings.contains(rawValue) {
- return decodedNSString as? String
- }
-
- else if rawValue > 1 {
- print("ENCODING FOUND \(rawValue)")
- }
-
- var macStr = String(data: subdata, encoding: .macOSRoman)
- if macStr == nil {
- macStr = String(data: subdata, encoding: .nonLossyASCII)
- }
-
- return macStr
+ return data.readString(at: 0, length: length)
}
func readPString(at offset: Int) -> (String?, Int) {
@@ -1158,36 +1131,39 @@ extension NetSocket {
let length = try await read(UInt8.self)
guard length > 0 else { return nil }
- let data = try await read(Int(length))
+ let dataLength = Int(length)
+ let data = try await read(dataLength)
+
+ return data.readString(at: 0, length: dataLength)
// Try auto-detection with common encodings
- let allowedEncodings = [
- String.Encoding.utf8.rawValue,
- String.Encoding.shiftJIS.rawValue,
- String.Encoding.unicode.rawValue,
- String.Encoding.windowsCP1251.rawValue
- ]
-
- var decodedString: NSString?
- let detected = NSString.stringEncoding(
- for: data,
- encodingOptions: [.allowLossyKey: false],
- convertedString: &decodedString,
- usedLossyConversion: nil
- )
-
- if allowedEncodings.contains(detected), let str = decodedString as? String {
- return str
- }
-
- // Fallback to MacRoman for classic Mac compatibility
- guard let str = String(data: data, encoding: .macOSRoman) else {
- throw NetSocketError.decodeFailed(NSError(
- domain: "NetSocket",
- code: -1,
- userInfo: [NSLocalizedDescriptionKey: "Failed to decode pascal string with any known encoding"]
- ))
- }
- return str
+// let allowedEncodings = [
+// String.Encoding.utf8.rawValue,
+// String.Encoding.shiftJIS.rawValue,
+// String.Encoding.unicode.rawValue,
+// String.Encoding.windowsCP1251.rawValue
+// ]
+//
+// var decodedString: NSString?
+// let detected = NSString.stringEncoding(
+// for: data,
+// encodingOptions: [.allowLossyKey: false],
+// convertedString: &decodedString,
+// usedLossyConversion: nil
+// )
+//
+// if allowedEncodings.contains(detected), let str = decodedString as? String {
+// return str
+// }
+//
+// // Fallback to MacRoman for classic Mac compatibility
+// guard let str = String(data: data, encoding: .macOSRoman) else {
+// throw NetSocketError.decodeFailed(NSError(
+// domain: "NetSocket",
+// code: -1,
+// userInfo: [NSLocalizedDescriptionKey: "Failed to decode pascal string with any known encoding"]
+// ))
+// }
+// return str
}
}
diff --git a/Hotline/Hotline/Transfers/HotlineFileUploadClient.swift b/Hotline/Hotline/Transfers/HotlineFileUploadClient.swift
index 384e9bd..937db6a 100644
--- a/Hotline/Hotline/Transfers/HotlineFileUploadClient.swift
+++ b/Hotline/Hotline/Transfers/HotlineFileUploadClient.swift
@@ -131,7 +131,9 @@ public class HotlineFileUploadClient: @MainActor HotlineTransferClient {
throw HotlineTransferClientError.failedToTransfer
}
- let infoForkData = infoFork.data()
+ guard let infoForkData = infoFork.data() else {
+ throw HotlineTransferClientError.failedToTransfer
+ }
let dataForkSize = forkSizes.dataForkSize
let resourceForkSize = forkSizes.resourceForkSize
diff --git a/Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift b/Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift
index 1947de6..f8dfe02 100644
--- a/Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift
+++ b/Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift
@@ -380,7 +380,10 @@ public class HotlineFolderUploadClient: @MainActor HotlineTransferClient {
throw HotlineTransferClientError.failedToTransfer
}
- let infoForkData = infoFork.data()
+ guard let infoForkData = infoFork.data() else {
+ throw HotlineTransferClientError.failedToTransfer
+ }
+
let dataForkSize = forkSizes.dataForkSize
let resourceForkSize = forkSizes.resourceForkSize
diff --git a/Hotline/Hotline/Transfers/HotlineTransferClient.swift b/Hotline/Hotline/Transfers/HotlineTransferClient.swift
index 596155b..5f786fc 100644
--- a/Hotline/Hotline/Transfers/HotlineTransferClient.swift
+++ b/Hotline/Hotline/Transfers/HotlineTransferClient.swift
@@ -260,8 +260,10 @@ struct HotlineFileInfoFork {
return nil
}
- func data() -> Data {
- let fileName = self.name.data(using: .macOSRoman)!
+ func data() -> Data? {
+ guard let fileName = self.name.data(using: .macOSRoman, allowLossyConversion: true) else {
+ return nil
+ }
let data = Data(endian: .big) {
self.platform
diff --git a/Hotline/MacApp.swift b/Hotline/MacApp.swift
index 4f6c2af..bf816d7 100644
--- a/Hotline/MacApp.swift
+++ b/Hotline/MacApp.swift
@@ -184,9 +184,11 @@ struct Application: App {
} defaultValue: {
Server(name: nil, description: nil, address: "")
}
- .modelContainer(self.modelContainer)
+ .windowToolbarStyle(.unified)
+// .windowStyle(.hiddenTitleBar)
.defaultSize(width: 780, height: 640)
.defaultPosition(.center)
+ .modelContainer(self.modelContainer)
.onChange(of: activeServerState) {
AppState.shared.activeServerState = self.activeServerState
}
diff --git a/Hotline/State/HotlineState.swift b/Hotline/State/HotlineState.swift
index 7baa477..df8434d 100644
--- a/Hotline/State/HotlineState.swift
+++ b/Hotline/State/HotlineState.swift
@@ -324,10 +324,10 @@ class HotlineState: Equatable {
print("HotlineState.login(): Getting server info...")
if let serverInfo = await client.server {
self.serverVersion = serverInfo.version
- if !serverInfo.name.isEmpty {
- self.serverName = serverInfo.name
+ if let name = serverInfo.name {
+ self.serverName = name
}
- print("HotlineState.login(): Server info retrieved: \(serverInfo.name) v\(serverInfo.version)")
+ print("HotlineState.login(): Server info retrieved: \(self.serverTitle) v\(serverInfo.version)")
}
self.status = .connected
@@ -336,7 +336,7 @@ class HotlineState: Equatable {
// Request initial data before starting event loop
print("HotlineState.login(): Requesting user list...")
try await self.getUserList()
-
+
self.status = .loggedIn
print("HotlineState.login(): Status set to loggedIn")
@@ -398,7 +398,6 @@ class HotlineState: Equatable {
}
/// Disconnect from the server (user-initiated)
- @MainActor
func disconnect() async {
print("HotlineState.disconnect(): Called")
guard let client = self.client else {
@@ -424,7 +423,6 @@ class HotlineState: Equatable {
}
/// Handle connection closure (server-initiated or after user disconnect)
- @MainActor
private func handleConnectionClosed() {
print("HotlineState: handleConnectionClosed() entered")
guard self.client != nil else {
@@ -830,8 +828,7 @@ class HotlineState: Equatable {
return try await client.getClientInfoText(for: userID)
}
catch let error as HotlineClientError {
- self.errorMessage = error.userMessage
- self.errorDisplayed = true
+ self.displayError(error, message: error.userMessage)
}
return nil
@@ -846,25 +843,33 @@ class HotlineState: Equatable {
try await client.disconnectUser(userID: userID, options: options)
}
catch let error as HotlineClientError {
- self.errorMessage = error.userMessage
- self.errorDisplayed = true
+ self.displayError(error, message: error.userMessage)
}
}
// MARK: - Files
@discardableResult
- func getFileList(path: [String] = [], suppressErrors: Bool = false, preferCache: Bool = false) async throws -> [FileInfo] {
+ func getFileList(path: [String] = [], suppressErrors: Bool = false, preferCache: Bool = false) async throws -> [FileInfo]? {
guard let client = self.client else {
throw HotlineClientError.notConnected
}
-
+
// Check cache first if preferred
if preferCache, let cached = self.cachedFileList(for: path, ttl: self.fileSearchConfig.cacheTTL, allowStale: false) {
return cached.items
}
-
- let hotlineFiles = try await client.getFileList(path: path)
+
+ let hotlineFiles: [HotlineFile]
+ do {
+ hotlineFiles = try await client.getFileList(path: path)
+ }
+ catch let error as HotlineClientError {
+ self.displayError(error, message: error.userMessage)
+ self.filesLoaded = true
+ return nil
+ }
+
let newFiles = hotlineFiles.map { FileInfo(hotlineFile: $0) }
// Update UI state
@@ -908,8 +913,7 @@ class HotlineState: Equatable {
return true
}
catch let error as HotlineClientError {
- self.errorMessage = error.userMessage
- self.errorDisplayed = true
+ self.displayError(error, message: error.userMessage)
}
return false
@@ -927,8 +931,7 @@ class HotlineState: Equatable {
return true
}
catch let error as HotlineClientError {
- self.errorMessage = error.userMessage
- self.errorDisplayed = true
+ self.displayError(error, message: error.userMessage)
}
return false
@@ -952,8 +955,7 @@ class HotlineState: Equatable {
return true
}
catch let error as HotlineClientError {
- self.errorMessage = error.userMessage
- self.errorDisplayed = true
+ self.displayError(error, message: error.userMessage)
}
return false
@@ -985,8 +987,7 @@ class HotlineState: Equatable {
result = try await client.downloadFile(name: fileName, path: fullPath)
}
catch let error as HotlineClientError {
- self.errorMessage = error.userMessage
- self.errorDisplayed = true
+ self.displayError(error, message: error.userMessage)
return
}
@@ -1351,6 +1352,8 @@ class HotlineState: Equatable {
guard let client = self.client else { return }
let fileName = fileURL.lastPathComponent
+
+ print("UPLOAD FILE: \(fileName) \(fileURL)")
guard fileURL.isFileURL, !fileName.isEmpty else {
print("HotlineState: Not a valid file URL")
@@ -1381,8 +1384,7 @@ class HotlineState: Equatable {
referenceNumber = try await client.uploadFile(name: fileName, path: path)
}
catch let error as HotlineClientError {
- self.errorMessage = error.userMessage
- self.errorDisplayed = true
+ self.displayError(error, message: error.userMessage)
return
}
diff --git a/Hotline/macOS/Board/MessageBoardView.swift b/Hotline/macOS/Board/MessageBoardView.swift
index 710ff20..be039d8 100644
--- a/Hotline/macOS/Board/MessageBoardView.swift
+++ b/Hotline/macOS/Board/MessageBoardView.swift
@@ -8,16 +8,14 @@ struct MessageBoardView: View {
var body: some View {
NavigationStack {
- if self.model.access?.contains(.canReadMessageBoard) != false {
- if self.model.messageBoardLoaded && self.model.messageBoard.isEmpty {
- self.emptyBoardView
- }
- else {
- self.messageBoardView
- }
+ if self.model.access?.contains(.canReadMessageBoard) != true {
+ self.disabledBoardView
+ }
+ else if self.model.messageBoardLoaded && self.model.messageBoard.isEmpty {
+ self.emptyBoardView
}
else {
- self.disabledBoardView
+ self.messageBoardView
}
}
.sheet(isPresented: $composerDisplayed) {
@@ -32,7 +30,7 @@ struct MessageBoardView: View {
} label: {
Image(systemName: "square.and.pencil")
}
- .disabled(self.model.access?.contains(.canPostMessageBoard) == false)
+ .disabled((self.model.access?.contains(.canPostMessageBoard) != true) || (self.model.access?.contains(.canReadMessageBoard) != true))
.help("Post to Message Board")
}
}
@@ -45,9 +43,9 @@ struct MessageBoardView: View {
private var disabledBoardView: some View {
ContentUnavailableView {
- Label("Message Board Disabled", systemImage: "quote.bubble")
+ Label("No Message Board", systemImage: "quote.bubble")
} description: {
- Text("This server has turned off the message board")
+ Text("This server has turned off their message board")
}
}
diff --git a/Hotline/macOS/Files/FolderItemView.swift b/Hotline/macOS/Files/FolderItemView.swift
index 18522af..f82bb11 100644
--- a/Hotline/macOS/Files/FolderItemView.swift
+++ b/Hotline/macOS/Files/FolderItemView.swift
@@ -20,7 +20,7 @@ struct FolderItemView: View {
self.model.uploadFile(url: fileURL, path: filePath) { info in
Task {
// Refresh file listing to display newly uploaded file.
- let _ = try? await model.getFileList(path: filePath)
+ try? await model.getFileList(path: filePath)
}
}
}
diff --git a/Hotline/macOS/MessageView.swift b/Hotline/macOS/MessageView.swift
index 6208d54..858aa37 100644
--- a/Hotline/macOS/MessageView.swift
+++ b/Hotline/macOS/MessageView.swift
@@ -30,9 +30,15 @@ struct MessageView: View {
}
.background(Color(nsColor: .textBackgroundColor))
.onAppear {
- let user = self.model.users.first(where: { $0.id == userID })
+ self.model.markInstantMessagesAsRead(userID: self.userID)
+
+ let user = self.model.users.first(where: { $0.id == self.userID })
self.username = user?.name
}
+
+ .onAppear {
+ self.model.markInstantMessagesAsRead(userID: userID)
+ }
.toolbar {
if self.model.access?.contains(.canGetClientInfo) == true {
ToolbarItem {
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index ff92acc..696e08d 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -111,6 +111,7 @@ struct ServerView: View {
self.connectForm
Spacer()
}
+ .presentedWindowToolbarStyle(.unified(showsTitle: false))
.navigationTitle(self.model.serverTitle.isBlank ? "Hotline" : self.model.serverTitle)
}
else if self.model.status.isLoggingIn {
@@ -249,6 +250,11 @@ struct ServerView: View {
if menuItem.type == .chat {
ListItemView(icon: menuItem.image, title: menuItem.name, unread: model.unreadPublicChat).tag(menuItem.type)
}
+// else if menuItem.type == .board {
+// if self.model.access?.contains(.canReadMessageBoard) == true {
+// ListItemView(icon: menuItem.image, title: menuItem.name, unread: false).tag(menuItem.type)
+// }
+// }
// else if menuItem.type == .accounts {
// if model.access?.contains(.canOpenUsers) == true {
// ListItemView(icon: menuItem.image, title: menuItem.name, unread: false).tag(menuItem.type)
@@ -355,40 +361,17 @@ struct ServerView: View {
switch state.selection {
case .chat:
ChatView()
- .navigationTitle(model.serverTitle)
-// .navigationSubtitle("Public Chat")
-// .navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .news:
NewsView()
- .navigationTitle(model.serverTitle)
-// .navigationSubtitle("Newsgroups")
-// .navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .board:
MessageBoardView()
- .navigationTitle(model.serverTitle)
-// .navigationSubtitle("Message Board")
-// .navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .files:
FilesView()
- .navigationTitle(model.serverTitle)
-// .navigationSubtitle("Shared Files")
-// .navigationSplitViewColumnWidth(min: 250, ideal: 500)
-// case .accounts:
-// AccountManagerView()
-// .navigationTitle(model.serverTitle)
-//// .navigationSubtitle("Accounts")
-// .navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .user(let userID):
-// let user = model.users.first(where: { $0.id == userID })
MessageView(userID: userID)
- .navigationTitle(model.serverTitle)
-// .navigationSubtitle(user?.name ?? "Private Message")
-// .navigationSplitViewColumnWidth(min: 250, ideal: 500)
- .onAppear {
- model.markInstantMessagesAsRead(userID: userID)
- }
}
}
+ .navigationTitle(self.model.serverTitle)
}
// MARK: -
@@ -538,7 +521,7 @@ struct ServerTransferRow: View {
withAnimation(.snappy(duration: 0.25, extraBounce: 0.3)) {
self.hovered = hovered
}
- self.detailsShown = hovered
+// self.detailsShown = hovered
}
.onTapGesture(count: 2) {
guard transfer.completed, let url = transfer.fileURL else {