diff options
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 14 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineExtensions.swift | 94 | ||||
| -rw-r--r-- | Hotline/Hotline/Transfers/HotlineFileUploadClient.swift | 4 | ||||
| -rw-r--r-- | Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift | 5 | ||||
| -rw-r--r-- | Hotline/Hotline/Transfers/HotlineTransferClient.swift | 6 | ||||
| -rw-r--r-- | Hotline/MacApp.swift | 4 | ||||
| -rw-r--r-- | Hotline/State/HotlineState.swift | 50 | ||||
| -rw-r--r-- | Hotline/macOS/Board/MessageBoardView.swift | 20 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FolderItemView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/MessageView.swift | 8 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 33 |
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 { |