From f71c4cae3b21db506573bcdfa9fdb6cde41cc0ca Mon Sep 17 00:00:00 2001 From: Dustin Mierau Date: Tue, 12 Dec 2023 12:58:38 -0800 Subject: Moved to new client/model separation so client can be used in other projects. Some UI tweaks. --- Hotline/Hotline/HotlineClient.swift | 686 +++++++++++++++++---------- Hotline/Hotline/HotlineClientModel.swift | 10 - Hotline/Hotline/HotlineNewClient.swift | 781 ------------------------------- Hotline/HotlineApp.swift | 20 +- Hotline/Info.plist | 29 ++ Hotline/Models/Hotline.swift | 64 ++- Hotline/Views/ChatView.swift | 3 +- Hotline/Views/FilesView.swift | 4 +- Hotline/Views/MessageBoardView.swift | 29 +- Hotline/Views/NewsView.swift | 53 +-- Hotline/Views/TrackerView.swift | 34 +- Hotline/Views/UsersView.swift | 2 +- 12 files changed, 605 insertions(+), 1110 deletions(-) delete mode 100644 Hotline/Hotline/HotlineClientModel.swift delete mode 100644 Hotline/Hotline/HotlineNewClient.swift create mode 100644 Hotline/Info.plist (limited to 'Hotline') diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index 9d686e2..40096dd 100644 --- a/Hotline/Hotline/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -9,9 +9,51 @@ enum HotlineClientStatus: Int { case loggedIn } -@Observable +enum HotlineTransactionError: Error { + case networkFailure + case error(UInt32, String?) + case invalidMessage(UInt32, String?) +} + +//struct HotlineTransactionError { +// let code: UInt32 +// let message: String +//} + +struct HotlineTransactionInfo { + let type: HotlineTransactionType + let callback: ((HotlineTransaction) -> Void)? + let reply: ((HotlineTransaction) -> Void)? +} + +//struct HotlineAccount { +// let username: String +// let iconID: UInt16 +//} + +protocol HotlineClientDelegate: AnyObject { + func hotlineGetUserInfo() -> (String, UInt16) + func hotlineStatusChanged(status: HotlineClientStatus) + func hotlineReceivedAgreement(text: String) + func hotlineReceivedChatMessage(message: String) + func hotlineReceivedUserList(users: [HotlineUser]) + func hotlineReceivedServerMessage(message: String) + func hotlineUserChanged(user: HotlineUser) + func hotlineUserDisconnected(userID: UInt16) +} + +extension HotlineClientDelegate { + func hotlineStatusChanged(status: HotlineClientStatus) {} + func hotlineReceivedAgreement(text: String) {} + func hotlineReceivedChatMessage(message: String) {} + func hotlineReceivedUserList(users: [HotlineUser]) {} + func hotlineReceivedServerMessage(message: String) {} + func hotlineUserChanged(user: HotlineUser) {} + func hotlineUserDisconnected(userID: UInt16) {} +} + class HotlineClient { -// static let shared = HotlineClient() + // static let shared = HotlineClient() static let handshakePacket = Data([ 0x54, 0x52, 0x54, 0x50, // 'TRTP' protocol ID @@ -19,35 +61,76 @@ class HotlineClient { 0x00, 0x01, // Version 0x00, 0x02, // Sub-version ]) - + + weak var delegate: HotlineClientDelegate? + var connectionStatus: HotlineClientStatus = .disconnected - var users: [UInt16:HotlineUser] = [:] - var userList: [HotlineUser] = [] - var chatMessages: [HotlineChat] = [] - var messageBoardMessages: [String] = [] - var fileList: [HotlineFile] = [] + var connectCallback: ((Bool) -> Void)? +// var chatMessages: [HotlineChat] = [] +// var messageBoardMessages: [String] = [] +// var fileList: [HotlineFile] = [] var newsCategories: [HotlineNewsCategory] = [] - var userName: String = "bolt" - var userIconID: UInt16 = 128 var serverVersion: UInt16 = 151 - var server: HotlineServer? - @ObservationIgnored private var connection: NWConnection? - @ObservationIgnored private var transactionLog: [UInt32:(HotlineTransactionType, ((HotlineTransaction) -> Void)?)] = [:] + private var connection: NWConnection? + private var transactionLog: [UInt32:(HotlineTransactionType, ((HotlineTransaction, HotlineTransactionError?) -> Void)?)] = [:] init() { -// let downloadsPath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask) -// print("DOWNLOAD TO: \(downloadsPath)") + // let downloadsPath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask) + // print("DOWNLOAD TO: \(downloadsPath)") } // MARK: - - func connect(to server: HotlineServer) { - self.server = server + func login(_ address: String, port: UInt16, login: String, password: String, username: String, iconID: UInt16, callback: ((HotlineTransactionError?, UInt16) -> Void)?) -> Bool { + print("AWAITING CONNECT") + self.connect(address: address, port: port) { [weak self] success in + guard success else { + DispatchQueue.main.async { + callback?(.networkFailure, 0) + } + return + } + + print("AWAITING HANDSHAKE") + self?.sendHandshake() { [weak self] success in + guard success else { + DispatchQueue.main.async { + callback?(.networkFailure, 0) + } + return + } + + print("AWAITING LOGIN") + self?.sendLogin(login: login, password: password, username: username, iconID: iconID) { [weak self] err, serverVersion in + guard err == nil else { + DispatchQueue.main.async { + callback?(err, 0) + } + return + } + + self?.serverVersion = serverVersion + print("SERVER VERSION: \(serverVersion)") + + self?.sendSetClientUserInfo(username: username, iconID: iconID) + self?.sendGetUserList() + + DispatchQueue.main.async { + callback?(nil, serverVersion) + } + + } + } + } - let serverAddress = NWEndpoint.Host(server.address) - let serverPort = NWEndpoint.Port(rawValue: server.port)! + return false + } + + private func connect(address: String, port: UInt16, callback: ((Bool) -> Void)?) { + let serverAddress = NWEndpoint.Host(address) + let serverPort = NWEndpoint.Port(rawValue: port)! let tcpOptions = NWProtocolTCP.Options() tcpOptions.enableKeepalive = true @@ -55,46 +138,94 @@ class HotlineClient { let connectionParameters: NWParameters connectionParameters = NWParameters(tls: nil, tcp: tcpOptions) + self.connectCallback = callback + self.connection = NWConnection(host: serverAddress, port: serverPort, using: connectionParameters) self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in + guard let self = self else { + return + } + switch newState { + case .preparing: + print("HotlineClient: connection preparing...") + case .setup: + print("HotlineClient: connection setup") + case .waiting(let err): + print("HotlineClient: connection waiting \(err)...") + switch err { + case .posix(let errCode): + switch errCode { + case .ETIMEDOUT: + self.disconnect() + case .ECONNREFUSED: + self.disconnect() + default: + self.disconnect() + break + } + + print("HotlineClient: posix error code \(errCode)") + default: + print("HotlineClient: error code \(err)") + + } case .ready: print("HotlineClient: connection ready!") - DispatchQueue.main.async { - self?.connectionStatus = .connected - } - self?.sendHandshake() - case .cancelled: - print("HotlineClient: connection cancelled") - DispatchQueue.main.async { - self?.connectionStatus = .disconnected - } - self?.reset() + self.updateConnectionStatus(.connected) + self.connectCallback?(true) + self.connectCallback = nil +// if self.connectionContinuation != nil { +// let continuation = self.connectionContinuation! +// self.connectionContinuation = nil +// callback?(true) +// continuation.resume(returning: true) +// } +// callback?(true) case .failed(let err): print("HotlineClient: connection error \(err)") - DispatchQueue.main.async { - self?.connectionStatus = .disconnected - } - self?.reset() + self.updateConnectionStatus(.disconnected) + self.reset() + self.connectCallback?(false) + self.connectCallback = nil +// callback?(false) +// if self.connectionContinuation != nil { +// let continuation = self.connectionContinuation! +// self.connectionContinuation = nil +// continuation.resume(returning: false) +// } + case .cancelled: + print("HotlineClient: connection cancelled") + self.updateConnectionStatus(.disconnected) + self.reset() + self.connectCallback?(false) + self.connectCallback = nil +// callback?(false) +// if self.connectionContinuation != nil { +// let continuation = self.connectionContinuation! +// self.connectionContinuation = nil +// continuation.resume(returning: false) +// } default: - print("HotlineClient: unhandled connection state \(newState)") + break } } - DispatchQueue.main.async { - self.connectionStatus = .connecting - } + self.updateConnectionStatus(.connecting) self.connection?.start(queue: .global()) + +// return await withCheckedContinuation { [weak self] continuation in +// self?.connectionContinuation = continuation +// self?.connection?.start(queue: .global()) +// } } - func reset() { + private func reset() { self.transactionLog = [:] DispatchQueue.main.async { - self.chatMessages = [] - self.users = [:] - self.userList = [] - self.messageBoardMessages = [] - self.fileList = [] +// self.chatMessages = [] +// self.messageBoardMessages = [] +// self.fileList = [] self.newsCategories = [] } } @@ -104,29 +235,40 @@ class HotlineClient { self.connection = nil } + private func updateConnectionStatus(_ status: HotlineClientStatus) { + self.connectionStatus = status + DispatchQueue.main.async { [weak self] in + self?.delegate?.hotlineStatusChanged(status: status) + } + } + // MARK: - - private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil, reply: ((HotlineTransaction) -> Void)? = nil) { + private func sendTransaction(_ t: HotlineTransaction, sent sentCallback: ((Bool) -> Void)? = nil, reply replyCallback: ((HotlineTransaction, HotlineTransactionError?) -> Void)? = nil) { guard let c = connection else { return } print("HotlineClient => \(t.id) \(t.type)") - self.transactionLog[t.id] = (t.type, reply) + if replyCallback != nil { + self.transactionLog[t.id] = (t.type, replyCallback) + } c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in - if disconnectOnError, error != nil { + if error != nil { + sentCallback?(false) + self?.transactionLog[t.id] = nil self?.disconnect() return } - callback?() + sentCallback?(true) }) } - private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil) { - sendTransaction(t, autodisconnect: disconnectOnError, callback: callback, reply: nil) + private func sendTransaction(_ t: HotlineTransaction, sent sentCallback: ((Bool) -> Void)? = nil) { + self.sendTransaction(t, sent: sentCallback, reply: nil) } private func receiveTransaction() { @@ -152,7 +294,7 @@ class HotlineClient { return } -// print("HotlineClient: received \(headerData.count) header bytes") + // print("HotlineClient: received \(headerData.count) header bytes") if var transaction = self.parseTransaction(data: headerData) { // Receive additional data if the transaction has data attached to it. @@ -182,7 +324,7 @@ class HotlineClient { if let fieldType = HotlineTransactionFieldType(rawValue: fieldID) { transaction.fields.append(HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldRemainingData)) -// transaction.parameters[fieldType] = HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldData) + // transaction.parameters[fieldType] = HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldData) } else { print("HotlineClient: UNKNOWN FIELD TYPE!", fieldID, fieldSize) @@ -216,7 +358,7 @@ class HotlineClient { } } } - + private func parseTransaction(data: Data) -> HotlineTransaction? { if let flags = data.readUInt8(at: 0), @@ -240,8 +382,8 @@ class HotlineClient { // MARK: - Messages - private func sendHandshake() { - guard let c = connection else { + private func sendHandshake(callback: ((Bool) -> Void)?) { + guard let c = self.connection else { print("HotlineClient: invalid connection to send handshake.") return } @@ -249,6 +391,7 @@ class HotlineClient { c.send(content: HotlineClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in if let err = error { print("HotlineClient: sending handshake failed \(err)") + callback?(false) return } print("HotlineClient: receiving handshake...") @@ -260,6 +403,7 @@ class HotlineClient { if data.isEmpty { print("HotlineClient: empty handshake response") self.disconnect() + callback?(false) return } @@ -267,6 +411,7 @@ class HotlineClient { if protocolID != 0x54525450 { // 'TRTP' print("HotlineClient: invalid handshake protocol ID \(protocolID)") self.disconnect() + callback?(false) return } @@ -274,244 +419,298 @@ class HotlineClient { if errorCode != 0 { // 0 == no error print("HotlineClient: handshake error", errorCode) self.disconnect() + callback?(false) return } + callback?(true) print("HotlineClient 🤝") - self.sendLogin() { [weak self] in - self?.sendSetClientUserInfo() - self?.sendGetUserList() - } self.receiveTransaction() } }) } - - func sendLogin(callback: (() -> Void)? = nil) { - DispatchQueue.main.async { - self.connectionStatus = .loggingIn - } + + func sendLogin(login: String, password: String, username: String, iconID: UInt16, callback: ((HotlineTransactionError?, UInt16) -> Void)?) { + self.updateConnectionStatus(.loggingIn) var t = HotlineTransaction(type: .login) - t.setFieldEncodedString(type: .userLogin, val: "") - t.setFieldEncodedString(type: .userPassword, val: "") - t.setFieldUInt16(type: .userIconID, val: self.userIconID) - t.setFieldString(type: .userName, val: self.userName) + t.setFieldEncodedString(type: .userLogin, val: login) + t.setFieldEncodedString(type: .userPassword, val: password) + t.setFieldUInt16(type: .userIconID, val: iconID) + t.setFieldString(type: .userName, val: username) t.setFieldUInt32(type: .versionNumber, val: 123) - - self.sendTransaction(t) { [weak self] in - DispatchQueue.main.async { - self?.connectionStatus = .loggedIn + + self.sendTransaction(t) { success in + if !success { + DispatchQueue.main.async { + callback?(.networkFailure, 0) + } } + } reply: { [weak self] replyTransaction, err in + print("GOT LOGIN REPLY") + self?.updateConnectionStatus(.loggedIn) - callback?() + var serverVersion: UInt16? + if + let serverVersionField = replyTransaction.getField(type: .versionNumber), + let serverVersionValue = serverVersionField.getUInt16() { + self?.serverVersion = serverVersionValue + serverVersion = serverVersionValue + print("SERVER VERSION: \(serverVersionValue)") + } + + DispatchQueue.main.async { + callback?(err, serverVersion ?? 0) + } } } - func sendSetClientUserInfo(callback: (() -> Void)? = nil) { + func sendSetClientUserInfo(username: String, iconID: UInt16, sent: ((Bool) -> Void)? = nil) { var t = HotlineTransaction(type: .setClientUserInfo) - t.setFieldString(type: .userName, val: self.userName) - t.setFieldUInt16(type: .userIconID, val: self.userIconID) - - self.sendTransaction(t, callback: callback) + t.setFieldString(type: .userName, val: username) + t.setFieldUInt16(type: .userIconID, val: iconID) + self.sendTransaction(t, sent: sent) } - func sendAgree(callback: (() -> Void)? = nil) { + func sendAgree(sent: ((Bool) -> Void)? = nil) { var t = HotlineTransaction(type: .agreed) - t.setFieldString(type: .userName, val: self.userName) - t.setFieldUInt16(type: .userIconID, val: self.userIconID) +// t.setFieldString(type: .userName, val: self.userName) +// t.setFieldUInt16(type: .userIconID, val: self.userIconID) t.setFieldUInt32(type: .options, val: 0) - self.sendTransaction(t, callback: callback) + self.sendTransaction(t, sent: sent) } - func sendChat(message: String, callback: (() -> Void)? = nil) { + func sendChat(message: String, sent sentCallback: ((Bool) -> Void)?) { var t = HotlineTransaction(type: .sendChat) t.setFieldString(type: .data, val: message) - self.sendTransaction(t, callback: callback) + self.sendTransaction(t, sent: sentCallback) } - func sendGetUserList(callback: (() -> Void)? = nil) { + func sendGetUserList(sent sentCallback: ((Bool) -> Void)? = nil) { + print("SENDING GET USER LIST") let t = HotlineTransaction(type: .getUserNameList) - self.sendTransaction(t, callback: callback) + self.sendTransaction(t, sent: sentCallback) { [weak self] replyTransaction, err in + print("GOT USER LIST") + var newUsers: [UInt16:HotlineUser] = [:] + var newUserList: [HotlineUser] = [] + for u in replyTransaction.getFieldList(type: .userNameWithInfo) { + let user = u.getUser() + newUsers[user.id] = user + newUserList.append(user) + } + DispatchQueue.main.async { [weak self] in + self?.delegate?.hotlineReceivedUserList(users: newUserList) + } + } } - func sendGetMessageBoard(callback: (() -> Void)? = nil) { + func sendGetMessageBoard(callback: ((HotlineTransactionError?, [String]) -> Void)?) { let t = HotlineTransaction(type: .getMessageBoard) - self.sendTransaction(t, callback: callback) + self.sendTransaction(t) { success in + if !success { + DispatchQueue.main.async { + callback?(.networkFailure, []) + } + } + } reply: { replyTransaction, err in + if err != nil { + DispatchQueue.main.async { + callback?(err, []) + } + return + } + + if let textField = replyTransaction.getField(type: .data), + let text = textField.getString() { + var messages: [String] = [] + let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/ + let matches = text.matches(of: messageBoardRegex) + var start = text.startIndex + + if matches.count > 0 { + for match in matches { + let range = match.range + messages.append(String(text[start.. Void)? = nil) { + func sendGetNewsCategories(sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsCategory]) -> Void)?) { let t = HotlineTransaction(type: .getNewsCategoryNameList) - self.sendTransaction(t, callback: callback) + self.sendTransaction(t, sent: sent, reply: { rt, err in + var categories: [HotlineNewsCategory] = [] + for categoryListItem in rt.getFieldList(type: .newsCategoryListData15) { + let c = categoryListItem.getNewsCategory() + categories.append(c) + print("CATEGORY: \(c)") + } + DispatchQueue.main.async { + reply?(categories) +// self.newsCategories = categories + } + }) } - func sendGetNewsArticles(path: [String]? = nil, callback: (() -> Void)? = nil) { + func sendGetNewsArticles(path: [String]? = nil, sent: ((Bool) -> Void)? = nil, reply: ((String) -> Void)? = nil) { var t = HotlineTransaction(type: .getNewsArticleNameList) if path != nil { t.setFieldPath(type: .newsPath, val: path!) } - self.sendTransaction(t, callback: callback) + self.sendTransaction(t, sent: sent) } - func sendGetFileList(path: [String] = [], callback: (() -> Void)? = nil, reply: (([HotlineFile]) -> Void)? = nil) { + func sendGetFileList(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineFile]) -> Void)? = nil) { var t = HotlineTransaction(type: .getFileNameList) - var parentFile: HotlineFile? = nil if !path.isEmpty { t.setFieldPath(type: .filePath, val: path) - parentFile = self.findFile(in: self.fileList, at: path) } - - -// if let p = path { -// t.setFieldString(type: .filePath) -// } - self.sendTransaction(t, callback: callback, reply: { r in + self.sendTransaction(t, sent: sent, reply: { r, err in + if err != nil { + reply?([]) + return + } + var files: [HotlineFile] = [] for fi in r.getFieldList(type: .fileNameWithInfo) { - var file = fi.getFile() + let file = fi.getFile() file.path = path + [file.name] files.append(file) } DispatchQueue.main.async { - if var pf = parentFile { - pf.files = files - } - else { - self.fileList = files - } reply?(files) } }) } - func findFile(in filesToSearch: [HotlineFile], at path: [String]) -> HotlineFile? { - guard !path.isEmpty, !filesToSearch.isEmpty else { return nil } - -// var stack: [([HotlineFile], [String])] = [(self.files!, path)] - - let currentName = path[0] - - for file in filesToSearch { - if file.name == currentName { - if path.count == 1 { - return file - } - else if let subfiles = file.files { - let remainingPath = Array(path[1...]) - return self.findFile(in: subfiles, at: remainingPath) - } - } - } - - return nil - } - -// func sendGetNews(callback: (() -> Void)? = nil) { -// let t = HotlineTransaction(type: .getNewsFile) -// self.sendTransaction(t, callback: callback) -// } + // func sendGetNews(callback: (() -> Void)? = nil) { + // let t = HotlineTransaction(type: .getNewsFile) + // self.sendTransaction(t, callback: callback) + // } // MARK: - Incoming private func processReply(_ transaction: HotlineTransaction) { - guard transaction.errorCode == 0 else { - if let errorParam = transaction.getField(type: .errorText), let errorText = errorParam.getString() { - print("HotlineClient 😵 \(transaction.errorCode): \(errorText)") - } - else { - print("HotlineClient 😵 \(transaction.errorCode)") - } + guard let replyCallbackInfo = self.transactionLog[transaction.id] else { return } - guard let repliedTransactionType = self.transactionLog[transaction.id] else { - return - } + self.transactionLog[transaction.id] = nil + + print("HotlineClient reply in response to \(replyCallbackInfo.0)") - defer { - let replyCallback = repliedTransactionType.1 +// var replyError: HotlineTransactionError? = nil + +// defer { +// let replyCallback = replyCallbackInfo.1 +// DispatchQueue.main.async { +// replyCallback?(transaction, replyError) +// } +// } + + let replyCallback = replyCallbackInfo.1 + + guard transaction.errorCode == 0 else { + let errorField: HotlineTransactionField? = transaction.getField(type: .errorText) + + print("HotlineClient 😵 \(transaction.errorCode): \(errorField?.getString() ?? "")") + DispatchQueue.main.async { - replyCallback?(transaction) + replyCallback?(transaction, .error(transaction.errorCode, errorField?.getString())) } + return } - self.transactionLog[transaction.id] = nil + replyCallback?(transaction, nil) - print("HotlineClient reply in response to \(repliedTransactionType)") - switch(repliedTransactionType.0) { - case .login: - print("GOT REPLY TO LOGIN!") +// switch(replyCallbackInfo.0) { +// case .login: +// print("GOT REPLY TO LOGIN!") - if - let serverVersionField = transaction.getField(type: .versionNumber), - let serverVersion = serverVersionField.getUInt16() { - self.serverVersion = serverVersion - print("SERVER VERSION: \(serverVersion)") - } - case .getUserNameList: - print("GOT USER LIST") - var newUsers: [UInt16:HotlineUser] = [:] - var newUserList: [HotlineUser] = [] - for u in transaction.getFieldList(type: .userNameWithInfo) { - let user = u.getUser() - newUsers[user.id] = user - newUserList.append(user) - } - DispatchQueue.main.async { - self.users = newUsers - self.userList = newUserList - - print("HotlineClient got users:\n") - print("\(self.userList)\n\n") - } - case .getMessageBoard: - if let textField = transaction.getField(type: .data), let text = textField.getString() { - var messages: [String] = [] - let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/ - let matches = text.matches(of: messageBoardRegex) - var start = text.startIndex - - if matches.count > 0 { - for match in matches { - let range = match.range - messages.append(String(text[start.. 0 { +// for match in matches { +// let range = match.range +// messages.append(String(text[start.. Void)? - let reply: ((HotlineTransaction) -> Void)? -} - -//struct HotlineAccount { -// let username: String -// let iconID: UInt16 -//} - -protocol HotlineNewClientDelegate: AnyObject { - func hotlineGetUserInfo() -> (String, UInt16) - func hotlineStatusChanged(status: HotlineNewClientStatus) - func hotlineReceivedAgreement(text: String) - func hotlineReceivedChatMessage(message: String) - func hotlineReceivedUserList(users: [HotlineUser]) - func hotlineReceivedServerMessage(message: String) - func hotlineUserChanged(user: HotlineUser) - func hotlineUserDisconnected(userID: UInt16) -} - -extension HotlineNewClientDelegate { - func hotlineStatusChanged(status: HotlineNewClientStatus) {} - func hotlineReceivedAgreement(text: String) {} - func hotlineReceivedChatMessage(message: String) {} - func hotlineReceivedUserList(users: [HotlineUser]) {} - func hotlineReceivedServerMessage(message: String) {} - func hotlineUserChanged(user: HotlineUser) {} - func hotlineUserDisconnected(userID: UInt16) {} -} - -class HotlineNewClient { - // static let shared = HotlineClient() - - static let handshakePacket = Data([ - 0x54, 0x52, 0x54, 0x50, // 'TRTP' protocol ID - 0x48, 0x4F, 0x54, 0x4C, // Sub-protocol ID - 0x00, 0x01, // Version - 0x00, 0x02, // Sub-version - ]) - - weak var delegate: HotlineNewClientDelegate? - - var connectionStatus: HotlineNewClientStatus = .disconnected - var connectCallback: ((Bool) -> Void)? -// var chatMessages: [HotlineChat] = [] -// var messageBoardMessages: [String] = [] - var fileList: [HotlineFile] = [] - var newsCategories: [HotlineNewsCategory] = [] - - // var username: String = "guest" - // var iconID: UInt16 - // var userIconID: UInt16 = 128 - var serverVersion: UInt16 = 151 - // var server: HotlineServer? - - private var connection: NWConnection? -// private var connectionContinuation: CheckedContinuation? - private var transactionLog: [UInt32:(HotlineTransactionType, ((HotlineTransaction, HotlineTransactionError?) -> Void)?)] = [:] - - init() { - // let downloadsPath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask) - // print("DOWNLOAD TO: \(downloadsPath)") - } - - // MARK: - - - func login(_ address: String, port: UInt16, login: String, password: String, username: String, iconID: UInt16, callback: ((HotlineTransactionError?, UInt16) -> Void)?) -> Bool { - print("AWAITING CONNECT") - self.connect(address: address, port: port) { [weak self] success in - guard success else { - DispatchQueue.main.async { - callback?(.networkFailure, 0) - } - return - } - - print("AWAITING HANDSHAKE") - self?.sendHandshake() { [weak self] success in - guard success else { - DispatchQueue.main.async { - callback?(.networkFailure, 0) - } - return - } - - print("AWAITING LOGIN") - self?.sendLogin(login: login, password: password, username: username, iconID: iconID) { [weak self] err, serverVersion in - guard err == nil else { - DispatchQueue.main.async { - callback?(err, 0) - } - return - } - - self?.serverVersion = serverVersion - print("SERVER VERSION: \(serverVersion)") - - self?.sendSetClientUserInfo(username: username, iconID: iconID) - self?.sendGetUserList() - - DispatchQueue.main.async { - callback?(nil, serverVersion) - } - - } - } - } - - return false - } - - private func connect(address: String, port: UInt16, callback: ((Bool) -> Void)?) { - let serverAddress = NWEndpoint.Host(address) - let serverPort = NWEndpoint.Port(rawValue: port)! - - let tcpOptions = NWProtocolTCP.Options() - tcpOptions.enableKeepalive = true - tcpOptions.keepaliveInterval = 30 - let connectionParameters: NWParameters - connectionParameters = NWParameters(tls: nil, tcp: tcpOptions) - - self.connectCallback = callback - - self.connection = NWConnection(host: serverAddress, port: serverPort, using: connectionParameters) - self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in - guard let self = self else { - return - } - - switch newState { - case .preparing: - print("HotlineClient: connection preparing...") - case .setup: - print("HotlineClient: connection setup") - case .waiting(let err): - print("HotlineClient: connection waiting \(err)...") - case .ready: - print("HotlineClient: connection ready!") - self.updateConnectionStatus(.connected) - self.connectCallback?(true) - self.connectCallback = nil -// if self.connectionContinuation != nil { -// let continuation = self.connectionContinuation! -// self.connectionContinuation = nil -// callback?(true) -// continuation.resume(returning: true) -// } -// callback?(true) - case .failed(let err): - print("HotlineClient: connection error \(err)") - self.updateConnectionStatus(.disconnected) - self.reset() - self.connectCallback?(false) - self.connectCallback = nil -// callback?(false) -// if self.connectionContinuation != nil { -// let continuation = self.connectionContinuation! -// self.connectionContinuation = nil -// continuation.resume(returning: false) -// } - case .cancelled: - print("HotlineClient: connection cancelled") - self.updateConnectionStatus(.disconnected) - self.reset() - self.connectCallback?(false) - self.connectCallback = nil -// callback?(false) -// if self.connectionContinuation != nil { -// let continuation = self.connectionContinuation! -// self.connectionContinuation = nil -// continuation.resume(returning: false) -// } - default: - break - } - } - - self.updateConnectionStatus(.connecting) - self.connection?.start(queue: .global()) - -// return await withCheckedContinuation { [weak self] continuation in -// self?.connectionContinuation = continuation -// self?.connection?.start(queue: .global()) -// } - } - - private func reset() { - self.transactionLog = [:] - DispatchQueue.main.async { -// self.chatMessages = [] -// self.messageBoardMessages = [] - self.fileList = [] - self.newsCategories = [] - } - } - - func disconnect() { - self.connection?.cancel() - self.connection = nil - } - - private func updateConnectionStatus(_ status: HotlineNewClientStatus) { - self.connectionStatus = status - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineStatusChanged(status: status) - } - } - - // MARK: - - - private func sendTransaction(_ t: HotlineTransaction, sent sentCallback: ((Bool) -> Void)? = nil, reply replyCallback: ((HotlineTransaction, HotlineTransactionError?) -> Void)? = nil) { - guard let c = connection else { - return - } - - print("HotlineClient => \(t.id) \(t.type)") - - if replyCallback != nil { - self.transactionLog[t.id] = (t.type, replyCallback) - } - - c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in - if error != nil { - sentCallback?(false) - self?.transactionLog[t.id] = nil - self?.disconnect() - return - } - - sentCallback?(true) - }) - } - - private func sendTransaction(_ t: HotlineTransaction, sent sentCallback: ((Bool) -> Void)? = nil) { - self.sendTransaction(t, sent: sentCallback, reply: nil) - } - - private func receiveTransaction() { - guard let c = connection else { - print("HotlineClient: no connection for transaction.") - return - } - - print("HotlineClient ⏳") - c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (headerData, context, isComplete, error) in - guard let self = self else { - return - } - - if let error = error { - print("HotlineClient: transaction error \(error)") - self.disconnect() - return - } - - guard let headerData = headerData, !headerData.isEmpty else { - self.receiveTransaction() - return - } - - // print("HotlineClient: received \(headerData.count) header bytes") - - if var transaction = self.parseTransaction(data: headerData) { - // Receive additional data if the transaction has data attached to it. - print("DATA SIZE: \(transaction.dataSize)") - if transaction.dataSize > 0 { - c.receive(minimumIncompleteLength: Int(transaction.dataSize), maximumLength: Int(transaction.dataSize)) { [weak self] (fieldData, context, isComplete, error) in - guard let self = self else { - return - } - - guard let fieldData = fieldData, !fieldData.isEmpty else { - print("HotlineClient: transaction field data is empty!") - self.disconnect() - return - } - - let fieldCount = fieldData.readUInt16(at: 0)! - - if fieldCount > 0 { - var dataCursor = 2 - for _ in 0.. HotlineTransaction? { - if - let flags = data.readUInt8(at: 0), - let isReply = data.readUInt8(at: 1), - let type = data.readUInt16(at: 2), - let id = data.readUInt32(at: 4), - let errorCode = data.readUInt32(at: 8), - let transactionSize = data.readUInt32(at: 12), - let dataSize = data.readUInt32(at: 16) { - - if let transactionType = HotlineTransactionType(rawValue: type) { - return HotlineTransaction(type: transactionType, flags: flags, isReply: isReply, id: id, errorCode: errorCode, totalSize: transactionSize, dataSize: dataSize) - } - else { - print("HotlineClient: Unknown type \(type) parsing with \(dataSize) bytes") - } - } - - return nil - } - - // MARK: - Messages - - private func sendHandshake(callback: ((Bool) -> Void)?) { - guard let c = self.connection else { - print("HotlineClient: invalid connection to send handshake.") - return - } - - c.send(content: HotlineNewClient.handshakePacket, completion: .contentProcessed { [weak self] (error) in - if let err = error { - print("HotlineClient: sending handshake failed \(err)") - callback?(false) - return - } - print("HotlineClient: receiving handshake...") - c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in - guard let self = self, let data = data else { - return - } - - if data.isEmpty { - print("HotlineClient: empty handshake response") - self.disconnect() - callback?(false) - return - } - - let protocolID = data.readUInt32(at: 0)! - if protocolID != 0x54525450 { // 'TRTP' - print("HotlineClient: invalid handshake protocol ID \(protocolID)") - self.disconnect() - callback?(false) - return - } - - let errorCode = data.readUInt32(at: 4)! - if errorCode != 0 { // 0 == no error - print("HotlineClient: handshake error", errorCode) - self.disconnect() - callback?(false) - return - } - - callback?(true) - print("HotlineClient 🤝") - self.receiveTransaction() - } - }) - } - - func sendLogin(login: String, password: String, username: String, iconID: UInt16, callback: ((HotlineTransactionError?, UInt16) -> Void)?) { - self.updateConnectionStatus(.loggingIn) - - var t = HotlineTransaction(type: .login) - t.setFieldEncodedString(type: .userLogin, val: login) - t.setFieldEncodedString(type: .userPassword, val: password) - t.setFieldUInt16(type: .userIconID, val: iconID) - t.setFieldString(type: .userName, val: username) - t.setFieldUInt32(type: .versionNumber, val: 123) - - self.sendTransaction(t) { success in - if !success { - DispatchQueue.main.async { - callback?(.networkFailure, 0) - } - } - } reply: { [weak self] replyTransaction, err in - print("GOT LOGIN REPLY") - self?.updateConnectionStatus(.loggedIn) - - var serverVersion: UInt16? - if - let serverVersionField = replyTransaction.getField(type: .versionNumber), - let serverVersionValue = serverVersionField.getUInt16() { - self?.serverVersion = serverVersionValue - serverVersion = serverVersionValue - print("SERVER VERSION: \(serverVersionValue)") - } - - DispatchQueue.main.async { - callback?(err, serverVersion ?? 0) - } - } - } - - func sendSetClientUserInfo(username: String, iconID: UInt16, sent: ((Bool) -> Void)? = nil) { - var t = HotlineTransaction(type: .setClientUserInfo) - t.setFieldString(type: .userName, val: username) - t.setFieldUInt16(type: .userIconID, val: iconID) - self.sendTransaction(t, sent: sent) - } - - func sendAgree(sent: ((Bool) -> Void)? = nil) { - var t = HotlineTransaction(type: .agreed) -// t.setFieldString(type: .userName, val: self.userName) -// t.setFieldUInt16(type: .userIconID, val: self.userIconID) - t.setFieldUInt32(type: .options, val: 0) - self.sendTransaction(t, sent: sent) - } - - func sendChat(message: String, sent sentCallback: ((Bool) -> Void)?) { - var t = HotlineTransaction(type: .sendChat) - t.setFieldString(type: .data, val: message) - self.sendTransaction(t, sent: sentCallback) - } - - func sendGetUserList(sent sentCallback: ((Bool) -> Void)? = nil) { - print("SENDING GET USER LIST") - let t = HotlineTransaction(type: .getUserNameList) - self.sendTransaction(t, sent: sentCallback) { [weak self] replyTransaction, err in - print("GOT USER LIST") - var newUsers: [UInt16:HotlineUser] = [:] - var newUserList: [HotlineUser] = [] - for u in replyTransaction.getFieldList(type: .userNameWithInfo) { - let user = u.getUser() - newUsers[user.id] = user - newUserList.append(user) - } - DispatchQueue.main.async { [weak self] in - self?.delegate?.hotlineReceivedUserList(users: newUserList) - } - } - } - - func sendGetMessageBoard(callback: ((HotlineTransactionError?, [String]) -> Void)?) { - let t = HotlineTransaction(type: .getMessageBoard) - self.sendTransaction(t) { success in - if !success { - DispatchQueue.main.async { - callback?(.networkFailure, []) - } - } - } reply: { replyTransaction, err in - if err != nil { - DispatchQueue.main.async { - callback?(err, []) - } - return - } - - if let textField = replyTransaction.getField(type: .data), - let text = textField.getString() { - var messages: [String] = [] - let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/ - let matches = text.matches(of: messageBoardRegex) - var start = text.startIndex - - if matches.count > 0 { - for match in matches { - let range = match.range - messages.append(String(text[start.. Void)? = nil) { - let t = HotlineTransaction(type: .getNewsCategoryNameList) - self.sendTransaction(t, sent: sent) - } - - func sendGetNewsArticles(path: [String]? = nil, sent: ((Bool) -> Void)? = nil, reply: ((String) -> Void)? = nil) { - var t = HotlineTransaction(type: .getNewsArticleNameList) - if path != nil { - t.setFieldPath(type: .newsPath, val: path!) - } - self.sendTransaction(t, sent: sent) - } - - func sendGetFileList(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineFile]) -> Void)? = nil) { - var t = HotlineTransaction(type: .getFileNameList) - - if !path.isEmpty { - t.setFieldPath(type: .filePath, val: path) - } - - self.sendTransaction(t, sent: sent, reply: { r, err in - if err != nil { - reply?([]) - return - } - - var files: [HotlineFile] = [] - for fi in r.getFieldList(type: .fileNameWithInfo) { - let file = fi.getFile() - file.path = path + [file.name] - files.append(file) - } - - DispatchQueue.main.async { - reply?(files) - } - }) - } - - // func sendGetNews(callback: (() -> Void)? = nil) { - // let t = HotlineTransaction(type: .getNewsFile) - // self.sendTransaction(t, callback: callback) - // } - - // MARK: - Incoming - - private func processReply(_ transaction: HotlineTransaction) { - guard let replyCallbackInfo = self.transactionLog[transaction.id] else { - return - } - - self.transactionLog[transaction.id] = nil - - print("HotlineClient reply in response to \(replyCallbackInfo.0)") - -// var replyError: HotlineTransactionError? = nil - -// defer { -// let replyCallback = replyCallbackInfo.1 -// DispatchQueue.main.async { -// replyCallback?(transaction, replyError) -// } -// } - - let replyCallback = replyCallbackInfo.1 - - guard transaction.errorCode == 0 else { - let errorField: HotlineTransactionField? = transaction.getField(type: .errorText) - - print("HotlineClient 😵 \(transaction.errorCode): \(errorField?.getString() ?? "")") - - DispatchQueue.main.async { - replyCallback?(transaction, .error(transaction.errorCode, errorField?.getString())) - } - return - } - - replyCallback?(transaction, nil) - - - switch(replyCallbackInfo.0) { -// case .login: -// print("GOT REPLY TO LOGIN!") - -// if -// let serverVersionField = transaction.getField(type: .versionNumber), -// let serverVersion = serverVersionField.getUInt16() { -// self.serverVersion = serverVersion -// print("SERVER VERSION: \(serverVersion)") -// } -// case .getUserNameList: -// print("GOT USER LIST") -// var newUsers: [UInt16:HotlineUser] = [:] -// var newUserList: [HotlineUser] = [] -// for u in transaction.getFieldList(type: .userNameWithInfo) { -// let user = u.getUser() -// newUsers[user.id] = user -// newUserList.append(user) -// } -// DispatchQueue.main.async { [weak self] in -//// self.userList = newUserList -// -// self?.delegate?.hotlineReceivedUserList(users: newUserList) -// } -// case .getMessageBoard: -// if let textField = transaction.getField(type: .data), let text = textField.getString() { -// var messages: [String] = [] -// let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/ -// let matches = text.matches(of: messageBoardRegex) -// var start = text.startIndex -// -// if matches.count > 0 { -// for match in matches { -// let range = match.range -// messages.append(String(text[start.. + + + + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + co.goodmake.hotline + CFBundleURLSchemes + + hotline + + + + CFBundleTypeRole + Viewer + CFBundleURLName + co.goodmake.hotline + CFBundleURLSchemes + + hotlinetracker + + + + + diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index 3272eb3..b2a9489 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -1,5 +1,38 @@ import SwiftUI +enum NewsCategoryType { + case bundle + case category +} + +@Observable class NewsCategory: Identifiable, Hashable { + let id: UUID = UUID() + + let name: String + let count: UInt16 + let type: NewsCategoryType + + init(hotlineNewsCategory: HotlineNewsCategory) { + self.name = hotlineNewsCategory.name + self.count = hotlineNewsCategory.count + + if hotlineNewsCategory.type == 2 { + self.type = .bundle + } + else { + self.type = .category + } + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } + + static func == (lhs: NewsCategory, rhs: NewsCategory) -> Bool { + return lhs.id == rhs.id + } +} + @Observable class FileInfo: Identifiable { let id: UUID = UUID() @@ -96,11 +129,11 @@ struct User: Identifiable { } } -@Observable final class Hotline: HotlineNewClientDelegate { +@Observable final class Hotline: HotlineClientDelegate { let trackerClient: HotlineTrackerClient - let client: HotlineNewClient + let client: HotlineClient - var status: HotlineNewClientStatus = .disconnected + var status: HotlineClientStatus = .disconnected var server: Server? = nil var serverVersion: UInt16? = nil @@ -111,10 +144,11 @@ struct User: Identifiable { var chat: [ChatMessage] = [] var messageBoard: [String] = [] var files: [FileInfo] = [] + var news: [NewsCategory] = [] // MARK: - - init(trackerClient: HotlineTrackerClient, client: HotlineNewClient) { + init(trackerClient: HotlineTrackerClient, client: HotlineClient) { self.trackerClient = trackerClient self.client = client self.client.delegate = self @@ -200,6 +234,25 @@ struct User: Identifiable { }) } } + + @MainActor func getNewsCategories() async -> [NewsCategory] { + return await withCheckedContinuation { [weak self] continuation in + self?.client.sendGetNewsCategories(sent: { success in + if !success { + continuation.resume(returning: []) + return + } + }, reply: { [weak self] categories in + var newCategories: [NewsCategory] = [] + for category in categories { + newCategories.append(NewsCategory(hotlineNewsCategory: category)) + } + self?.news = newCategories + + continuation.resume(returning: newCategories) + }) + } + } // @MainActor func updateUsers() async -> [User] { @@ -212,7 +265,7 @@ struct User: Identifiable { // MARK: - Hotline Delegate - func hotlineStatusChanged(status: HotlineNewClientStatus) { + func hotlineStatusChanged(status: HotlineClientStatus) { print("Hotline: Connection status changed to: \(status)") if status == .disconnected { @@ -221,6 +274,7 @@ struct User: Identifiable { self.chat = [] self.messageBoard = [] self.files = [] + self.news = [] } self.status = status diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift index ce3942e..60951c3 100644 --- a/Hotline/Views/ChatView.swift +++ b/Hotline/Views/ChatView.swift @@ -7,7 +7,6 @@ extension View { } struct ChatView: View { -// @Environment(HotlineClient.self) private var hotline @Environment(Hotline.self) private var model: Hotline @Environment(\.colorScheme) var colorScheme @@ -180,5 +179,5 @@ struct ChatView: View { #Preview { ChatView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineNewClient())) + .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) } diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift index 11db93c..23b19b1 100644 --- a/Hotline/Views/FilesView.swift +++ b/Hotline/Views/FilesView.swift @@ -2,7 +2,6 @@ import SwiftUI import UniformTypeIdentifiers struct FileView: View { -// @Environment(HotlineClient.self) private var hotline @Environment(Hotline.self) private var model: Hotline @State var expanded = false @@ -84,7 +83,6 @@ struct FileView: View { } struct FilesView: View { -// @Environment(HotlineClient.self) private var hotline @Environment(Hotline.self) private var model: Hotline @State var initialLoad = false @@ -125,5 +123,5 @@ struct FilesView: View { #Preview { FilesView() - .environment(HotlineClient()) + .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) } diff --git a/Hotline/Views/MessageBoardView.swift b/Hotline/Views/MessageBoardView.swift index 473162e..8a1c831 100644 --- a/Hotline/Views/MessageBoardView.swift +++ b/Hotline/Views/MessageBoardView.swift @@ -2,13 +2,11 @@ import SwiftUI struct MessageBoardView: View { @Environment(Hotline.self) private var model: Hotline -// @Environment(HotlineState.self) private var appState -// @Environment(HotlineClient.self) private var hotline + @State private var initialLoadComplete = false @State private var fetched = false var body: some View { -// @Bindable var config = appState NavigationStack { ScrollView { LazyVStack(alignment: .leading) { @@ -23,16 +21,23 @@ struct MessageBoardView: View { Spacer() } .task { - if !fetched { + if !initialLoadComplete { let _ = await model.getMessageBoard() -// hotline.sendGetMessageBoard() { - fetched = true -// } + initialLoadComplete = true + } + } + .overlay { + if !initialLoadComplete { + VStack { + ProgressView() + .controlSize(.large) + } + .frame(maxWidth: .infinity) } } .refreshable { let _ = await model.getMessageBoard() -// hotline.sendGetMessageBoard() + initialLoadComplete = true } .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -43,7 +48,6 @@ struct MessageBoardView: View { ToolbarItem(placement: .navigationBarLeading) { Button { model.disconnect() -// hotline.disconnect() } label: { Text(Image(systemName: "xmark.circle.fill")) .symbolRenderingMode(.hierarchical) @@ -56,10 +60,7 @@ struct MessageBoardView: View { } label: { Image(systemName: "square.and.pencil") -// .symbolRenderingMode(.hierarchical) -// .foregroundColor(.secondary) } - } } } @@ -69,7 +70,5 @@ struct MessageBoardView: View { #Preview { MessageBoardView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineNewClient())) -// .environment(HotlineState()) -// .environment(HotlineClient()) + .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) } diff --git a/Hotline/Views/NewsView.swift b/Hotline/Views/NewsView.swift index 539ead8..431b3ce 100644 --- a/Hotline/Views/NewsView.swift +++ b/Hotline/Views/NewsView.swift @@ -1,12 +1,11 @@ import SwiftUI struct NewsView: View { - @Environment(HotlineState.self) private var appState - @Environment(HotlineClient.self) private var hotline + @Environment(Hotline.self) private var model: Hotline @Environment(\.colorScheme) var colorScheme @State private var fetched = false - @State private var selectedCategory: HotlineNewsCategory? = nil + @State private var selectedCategory: NewsCategory? = nil @State private var topListHeight: CGFloat = 200 @State private var dividerHeight: CGFloat = 30 @@ -14,17 +13,17 @@ struct NewsView: View { // Your list content goes here List { - ForEach(hotline.newsCategories, id: \.self) { cat in + ForEach(model.news, id: \.self) { category in DisclosureGroup { ProgressView(value: 0.4) .task { - print("EXPANDED?", cat.name) - hotline.sendGetNewsArticles(path: [cat.name]) { - print("OK") - } + print("EXPANDED?", category.name) +// hotline.sendGetNewsArticles(path: [cat.name]) { +// print("OK") +// } } } label: { - Text(cat.name) + Text(category.name) .fontWeight(.medium) .lineLimit(1) .truncationMode(.tail) @@ -60,13 +59,8 @@ struct NewsView: View { .fill(.tertiary) .frame(width: 50, height: 6, alignment: .center) .cornerRadius(10) -// .opacity(0.3) -// Image(systemName: "line.3.horizontal") -// .opacity(0.2) -// .font(.system(size: 14)) } Spacer() -// Divider() } .background(colorScheme == .dark ? Color(white: 0.1) : Color(uiColor: UIColor.systemBackground)) .frame(maxWidth: .infinity) @@ -83,9 +77,8 @@ struct NewsView: View { } .task { if !fetched { - hotline.sendGetNewsCategories() { - fetched = true - } + let _ = await model.getNewsCategories() + fetched = true // hotline.sendGetNewsArticles(path: ["News"]) { // print("GOT ARTICLES?") @@ -98,12 +91,12 @@ struct NewsView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { - Text(hotline.server?.name ?? "") + Text(model.server?.name ?? "") .font(.headline) } ToolbarItem(placement: .navigationBarLeading) { Button { - hotline.disconnect() + model.disconnect() } label: { Text(Image(systemName: "xmark.circle.fill")) .symbolRenderingMode(.hierarchical) @@ -111,16 +104,16 @@ struct NewsView: View { .foregroundColor(.secondary) } } - ToolbarItem(placement: .navigationBarTrailing) { - Button { - - } label: { - Image(systemName: "square.and.pencil") - // .symbolRenderingMode(.hierarchical) - // .foregroundColor(.secondary) - } - - } +// ToolbarItem(placement: .navigationBarTrailing) { +// Button { +// +// } label: { +// Image(systemName: "square.and.pencil") +// // .symbolRenderingMode(.hierarchical) +// // .foregroundColor(.secondary) +// } +// +// } } } @@ -130,5 +123,5 @@ struct NewsView: View { #Preview { MessageBoardView() .environment(HotlineState()) - .environment(HotlineClient()) + .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) } diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift index 2fc27c7..01f4311 100644 --- a/Hotline/Views/TrackerView.swift +++ b/Hotline/Views/TrackerView.swift @@ -11,7 +11,7 @@ struct TrackerConnectView: View { @State private var password = "" @State private var connecting = false - func connectionStatusToProgress(status: HotlineNewClientStatus) -> Double { + func connectionStatusToProgress(status: HotlineClientStatus) -> Double { switch status { case .disconnected: return 0.0 @@ -26,8 +26,6 @@ struct TrackerConnectView: View { } } - - var body: some View { VStack(alignment: .leading) { if !connecting { @@ -176,7 +174,7 @@ struct TrackerView: View { return desc.count > 0 && desc != server.name } - func connectionStatusToProgress(status: HotlineNewClientStatus) -> Double { + func connectionStatusToProgress(status: HotlineClientStatus) -> Double { switch status { case .disconnected: return 0.0 @@ -196,7 +194,12 @@ struct TrackerView: View { } func updateServers() async { - self.servers = await model.getServers(address: "tracker.preterhuman.net") +// "hltracker.com" +// "tracker.preterhuman.net" +// "hotline.ubersoft.org" +// "tracked.nailbat.com" +// "hotline.duckdns.org" + self.servers = await model.getServers(address: "tracked.agent79.org") } var body: some View { @@ -347,11 +350,28 @@ struct TrackerView: View { await updateServers() initialLoadComplete = true } + .onOpenURL(perform: { url in + guard url.scheme == "hotline" else { + return + } + + if let address = url.host() { + let login = url.user(percentEncoded: false) ?? "" + let password = url.password(percentEncoded: false) ?? "" + let port = url.port ?? Server.defaultPort + + Task { + model.disconnect() + let _ = await model.login(server: Server(name: address, description: nil, address: address, port: port, users: 0), login: login, password: password, username: "bolt", iconID: 128) + } + + // TODO: Find a better way to show login status when trying to connect outside of the Tracker server list. Perhaps this opens the connect sheet prefilled. + } + }) } } #Preview { TrackerView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineNewClient())) - // .modelContainer(for: Item.self, inMemory: true) + .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) } diff --git a/Hotline/Views/UsersView.swift b/Hotline/Views/UsersView.swift index 0397ef7..ad43131 100644 --- a/Hotline/Views/UsersView.swift +++ b/Hotline/Views/UsersView.swift @@ -37,5 +37,5 @@ struct UsersView: View { #Preview { ChatView() - .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineNewClient())) + .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) } -- cgit