aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-12-12 12:58:38 -0800
committerDustin Mierau <dustin@mierau.me>2023-12-12 12:58:38 -0800
commitf71c4cae3b21db506573bcdfa9fdb6cde41cc0ca (patch)
treefb26e9ef242ef390d444264698758b635c77d01c /Hotline
parent18c4b8643df09be4ce52a4b110ea21ac1ad053fc (diff)
Moved to new client/model separation so client can be used in other projects. Some UI tweaks.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Hotline/HotlineClient.swift686
-rw-r--r--Hotline/Hotline/HotlineClientModel.swift10
-rw-r--r--Hotline/Hotline/HotlineNewClient.swift781
-rw-r--r--Hotline/HotlineApp.swift20
-rw-r--r--Hotline/Info.plist29
-rw-r--r--Hotline/Models/Hotline.swift64
-rw-r--r--Hotline/Views/ChatView.swift3
-rw-r--r--Hotline/Views/FilesView.swift4
-rw-r--r--Hotline/Views/MessageBoardView.swift29
-rw-r--r--Hotline/Views/NewsView.swift53
-rw-r--r--Hotline/Views/TrackerView.swift34
-rw-r--r--Hotline/Views/UsersView.swift2
12 files changed, 605 insertions, 1110 deletions
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..<range.lowerBound]))
+ start = range.upperBound
+ }
+ }
+ else {
+ messages.append(text)
+ }
+
+ DispatchQueue.main.async {
+ callback?(err, messages)
+ }
+
+
+
+// continuation.resume(returning: messages)
+// DispatchQueue.main.async {
+// self.messageBoardMessages = messages
+// }
+ }
+ }
}
- func sendGetNewsCategories(callback: (() -> 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..<range.lowerBound]))
- start = range.upperBound
- }
- }
- else {
- messages.append(text)
- }
-
- DispatchQueue.main.async {
- self.messageBoardMessages = messages
- }
- }
-// case .getFileNameList:
-// var files: [HotlineFile] = []
-// for fi in transaction.getFieldList(type: .fileNameWithInfo) {
-// let file = fi.getFile()
-// files.append(file)
+// 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..<range.lowerBound]))
+// start = range.upperBound
+// }
+// }
+// else {
+// messages.append(text)
+// }
+//
+// DispatchQueue.main.async {
+// self.messageBoardMessages = messages
+// }
+// }
+ // case .getFileNameList:
+ // var files: [HotlineFile] = []
+ // for fi in transaction.getFieldList(type: .fileNameWithInfo) {
+ // let file = fi.getFile()
+ // files.append(file)
+ // }
+ // DispatchQueue.main.async {
+ // self.fileList = files
+ // }
+// case .getNewsCategoryNameList:
+// var categories: [HotlineNewsCategory] = []
+// for fi in transaction.getFieldList(type: .newsCategoryListData15) {
+// let c = fi.getNewsCategory()
+// categories.append(c)
+// print("CATEGORY: \(c)")
// }
// DispatchQueue.main.async {
-// self.fileList = files
+// self.newsCategories = categories
// }
- case .getNewsCategoryNameList:
- var categories: [HotlineNewsCategory] = []
- for fi in transaction.getFieldList(type: .newsCategoryListData15) {
- let c = fi.getNewsCategory()
- categories.append(c)
- print("CATEGORY: \(c)")
- }
- DispatchQueue.main.async {
- self.newsCategories = categories
- }
- default:
- break
- }
+// default:
+// break
+// }
}
@@ -528,52 +727,65 @@ class HotlineClient {
switch(transaction.type) {
case .reply:
self.processReply(transaction)
-// print("HotlineClient: Received reply transaction: \(transaction)")
case .chatMessage:
print("HotlineClient: chat \(transaction)")
if
let chatTextParam = transaction.getField(type: .data),
let chatText = chatTextParam.getString()
-// let userNameParam = transaction.getField(type: .userName),
-// let userName = userNameParam.getString(),
-// let userIDParam = transaction.getField(type: .userID),
-// let userID = userIDParam.getUInt16() {
{
print("HotlineClient: \(chatText)")
- DispatchQueue.main.async {
- self.chatMessages.append(HotlineChat(text: chatText, type: .message))
- }
+ DispatchQueue.main.async { [weak self] in
+ self?.delegate?.hotlineReceivedChatMessage(message: chatText)
}
+ }
case .notifyOfUserChange:
-// print("HotlineClient: user changed")
- if let p = transaction.getField(type: .userName),
- let userName = p.getString() {
- print("HotlineClient: User changed \(userName)")
+ if let usernameField = transaction.getField(type: .userName),
+ let username = usernameField.getString(),
+ let userIDField = transaction.getField(type: .userID),
+ let userID = userIDField.getUInt16(),
+ let userIconIDField = transaction.getField(type: .userIconID),
+ let userIconID = userIconIDField.getUInt16(),
+ let userFlagsField = transaction.getField(type: .userFlags),
+ let userFlags = userFlagsField.getUInt16() {
+ print("HotlineClient: User changed \(username)")
+
+ let user = HotlineUser(id: userID, iconID: userIconID, status: userFlags, name: username)
+
+ DispatchQueue.main.async { [weak self] in
+ self?.delegate?.hotlineUserChanged(user: user)
+ }
+ }
+ case .notifyOfUserDelete:
+ if let userIDField = transaction.getField(type: .userID),
+ let userID = userIDField.getUInt16() {
+ DispatchQueue.main.async { [weak self] in
+ self?.delegate?.hotlineUserDisconnected(userID: userID)
+ }
}
case .disconnectMessage:
print("HotlineClient ❌")
self.disconnect()
+ case .serverMessage:
+ if let messageField = transaction.getField(type: .data),
+ let message = messageField.getString() {
+ DispatchQueue.main.async { [weak self] in
+ self?.delegate?.hotlineReceivedServerMessage(message: message)
+ }
+ }
case .showAgreement:
if let _ = transaction.getField(type: .noServerAgreement) {
- print("NO AGREEMENT?")
+ return
}
if let agreementParam = transaction.getField(type: .data) {
if let agreementText = agreementParam.getString() {
- print("\n\n--------------------------\n")
- print(agreementText)
- print("\n--------------------------\n\n")
- DispatchQueue.main.async {
- self.chatMessages.insert(HotlineChat(text: agreementText, type: .agreement), at: 0)
-// self.agreement = agreementText
+ DispatchQueue.main.async { [weak self] in
+ self?.delegate?.hotlineReceivedAgreement(text: agreementText)
}
-// self.sendAgree() {
-// self.sendGetUserList()
-// }
}
}
case .userAccess:
- print("")
+ print("HotlineClient: user access info \(transaction.getField(type: .userAccess).debugDescription)")
default:
print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.fields.count) parameters")
print(transaction.fields)
diff --git a/Hotline/Hotline/HotlineClientModel.swift b/Hotline/Hotline/HotlineClientModel.swift
deleted file mode 100644
index a45a5e2..0000000
--- a/Hotline/Hotline/HotlineClientModel.swift
+++ /dev/null
@@ -1,10 +0,0 @@
-import SwiftUI
-
-@Observable
-class HotlineClientModel {
- var status: HotlineClientStatus = .disconnected
- var server: HotlineServer?
- var userlist: [HotlineUser]?
- var chat: [(HotlineUser, String)]?
- var agreement: String?
-}
diff --git a/Hotline/Hotline/HotlineNewClient.swift b/Hotline/Hotline/HotlineNewClient.swift
deleted file mode 100644
index 2861fbc..0000000
--- a/Hotline/Hotline/HotlineNewClient.swift
+++ /dev/null
@@ -1,781 +0,0 @@
-import Foundation
-import Network
-
-enum HotlineNewClientStatus: Int {
- case disconnected
- case connecting
- case connected
- case loggingIn
- case loggedIn
-}
-
-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 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<Bool, Never>?
- 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..<fieldCount {
- if
- let fieldID = fieldData.readUInt16(at: dataCursor),
- let fieldSize = fieldData.readUInt16(at: dataCursor + 2),
- let fieldRemainingData = fieldData.readData(at: dataCursor + 4, length: Int(fieldSize)) {
-
- 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)
- }
- else {
- print("HotlineClient: UNKNOWN FIELD TYPE!", fieldID, fieldSize)
- }
-
- dataCursor += 4 + Int(fieldSize)
- }
- }
-
- // Process the transaction if we have processed more than zero parameters here
- // as we expect parameters at this point.
- self.processTransaction(transaction)
- }
-
- // Continue receiving transactions.
- self.receiveTransaction()
- }
- }
- else {
- // In this case we have no further data to receive so we simply
- // process the transaction and then continue receiving.
- self.processTransaction(transaction)
- self.receiveTransaction()
- }
- }
- else {
- // Here we failed to parse the current transaction.
- // We should consider disconnecting perhaps.
- // But for now we'll continue receiving.
- self.receiveTransaction()
- }
- }
- }
-
- private func parseTransaction(data: Data) -> 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..<range.lowerBound]))
- start = range.upperBound
- }
- }
- else {
- messages.append(text)
- }
-
- DispatchQueue.main.async {
- callback?(err, messages)
- }
-
-
-
-// continuation.resume(returning: messages)
-// DispatchQueue.main.async {
-// self.messageBoardMessages = messages
-// }
- }
- }
- }
-
- func sendGetNewsCategories(sent: ((Bool) -> 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..<range.lowerBound]))
-// start = range.upperBound
-// }
-// }
-// else {
-// messages.append(text)
-// }
-//
-// DispatchQueue.main.async {
-// self.messageBoardMessages = messages
-// }
-// }
- // case .getFileNameList:
- // var files: [HotlineFile] = []
- // for fi in transaction.getFieldList(type: .fileNameWithInfo) {
- // let file = fi.getFile()
- // files.append(file)
- // }
- // DispatchQueue.main.async {
- // self.fileList = files
- // }
- case .getNewsCategoryNameList:
- var categories: [HotlineNewsCategory] = []
- for fi in transaction.getFieldList(type: .newsCategoryListData15) {
- let c = fi.getNewsCategory()
- categories.append(c)
- print("CATEGORY: \(c)")
- }
- DispatchQueue.main.async {
- self.newsCategories = categories
- }
- default:
- break
- }
-
-
- }
-
- private func processTransaction(_ transaction: HotlineTransaction) {
- if transaction.type == .reply {
- print("HotlineClient <= \(transaction.type) to \(transaction.id):")
- print(transaction)
- }
- else {
- print("HotlineClient <= \(transaction.type)")
- }
-
- switch(transaction.type) {
- case .reply:
- self.processReply(transaction)
- // print("HotlineClient: Received reply transaction: \(transaction)")
-
- case .chatMessage:
- print("HotlineClient: chat \(transaction)")
- if
- let chatTextParam = transaction.getField(type: .data),
- let chatText = chatTextParam.getString()
- // let userNameParam = transaction.getField(type: .userName),
- // let userName = userNameParam.getString(),
- // let userIDParam = transaction.getField(type: .userID),
- // let userID = userIDParam.getUInt16() {
- {
- print("HotlineClient: \(chatText)")
- DispatchQueue.main.async { [weak self] in
- self?.delegate?.hotlineReceivedChatMessage(message: chatText)
-// self.chatMessages.append(HotlineChat(text: chatText, type: .message))
- }
- }
- case .notifyOfUserChange:
- // print("HotlineClient: user changed")
- if let usernameField = transaction.getField(type: .userName),
- let username = usernameField.getString(),
- let userIDField = transaction.getField(type: .userID),
- let userID = userIDField.getUInt16(),
- let userIconIDField = transaction.getField(type: .userIconID),
- let userIconID = userIconIDField.getUInt16(),
- let userFlagsField = transaction.getField(type: .userFlags),
- let userFlags = userFlagsField.getUInt16() {
- print("HotlineClient: User changed \(username)")
-
- let user = HotlineUser(id: userID, iconID: userIconID, status: userFlags, name: username)
-
- DispatchQueue.main.async { [weak self] in
- self?.delegate?.hotlineUserChanged(user: user)
- }
- }
- case .notifyOfUserDelete:
- if let userIDField = transaction.getField(type: .userID),
- let userID = userIDField.getUInt16() {
- DispatchQueue.main.async { [weak self] in
- self?.delegate?.hotlineUserDisconnected(userID: userID)
- }
- }
- case .disconnectMessage:
- print("HotlineClient ❌")
- self.disconnect()
- case .serverMessage:
- if let messageField = transaction.getField(type: .data),
- let message = messageField.getString() {
- DispatchQueue.main.async { [weak self] in
- self?.delegate?.hotlineReceivedServerMessage(message: message)
- }
- }
- case .showAgreement:
- if let _ = transaction.getField(type: .noServerAgreement) {
- print("NO AGREEMENT?")
- }
- if let agreementParam = transaction.getField(type: .data) {
- if let agreementText = agreementParam.getString() {
- print("\n\n--------------------------\n")
- print(agreementText)
- print("\n--------------------------\n\n")
- DispatchQueue.main.async { [weak self] in
- self?.delegate?.hotlineReceivedAgreement(text: agreementText)
- }
- }
- }
- case .userAccess:
- print("")
- default:
- print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.fields.count) parameters")
- print(transaction.fields)
- }
- }
-}
diff --git a/Hotline/HotlineApp.swift b/Hotline/HotlineApp.swift
index e6c3ddc..61f44bb 100644
--- a/Hotline/HotlineApp.swift
+++ b/Hotline/HotlineApp.swift
@@ -3,33 +3,15 @@ import SwiftData
@main
struct HotlineApp: App {
-// var sharedModelContainer: ModelContainer = {
-// let schema = Schema([
-// Item.self,
-// ])
-// let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
-//
-// do {
-// return try ModelContainer(for: schema, configurations: [modelConfiguration])
-// } catch {
-// fatalError("Could not create ModelContainer: \(error)")
-// }
-// }()
-
@State private var appState = HotlineState()
- @State private var hotline = HotlineClient()
- private var model = Hotline(trackerClient: HotlineTrackerClient(), client: HotlineNewClient())
+ private var model = Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())
var body: some Scene {
-
WindowGroup {
TrackerView()
.environment(appState)
- .environment(hotline)
-// .environment(tracker)
.environment(model)
}
-// .modelContainer(sharedModelContainer)
}
}
diff --git a/Hotline/Info.plist b/Hotline/Info.plist
new file mode 100644
index 0000000..8b58335
--- /dev/null
+++ b/Hotline/Info.plist
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>CFBundleURLName</key>
+ <string>co.goodmake.hotline</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>hotline</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>CFBundleURLName</key>
+ <string>co.goodmake.hotline</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>hotlinetracker</string>
+ </array>
+ </dict>
+ </array>
+</dict>
+</plist>
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()))
}