aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-12-16 09:16:15 -0800
committerDustin Mierau <dustin@mierau.me>2023-12-16 09:16:15 -0800
commit3e4fbdfcab4a4d2435aed27a16131a6ca26d7408 (patch)
treea1aec47c2de30074257dec14e3b2615a8b178033 /Hotline
parenta428e3e28fb851cec65ff27f212c19bce08e5369 (diff)
Implemented basic news reading support. Various UI tweaks.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.pngbin8190 -> 0 bytes
-rw-r--r--Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json2
-rw-r--r--Hotline/Assets.xcassets/AppIcon.appiconset/hotline app icon.pngbin0 -> 27684 bytes
-rw-r--r--Hotline/Hotline/HotlineClient.swift64
-rw-r--r--Hotline/Hotline/HotlineProtocol.swift117
-rw-r--r--Hotline/Hotline/HotlineTrackerClient.swift2
-rw-r--r--Hotline/Models/ChatMessage.swift2
-rw-r--r--Hotline/Models/Hotline.swift206
-rw-r--r--Hotline/Models/NewsCategory.swift34
-rw-r--r--Hotline/Models/NewsInfo.swift58
-rw-r--r--Hotline/Models/Server.swift4
-rw-r--r--Hotline/Views/ChatView.swift5
-rw-r--r--Hotline/Views/FilesView.swift2
-rw-r--r--Hotline/Views/MessageBoardView.swift2
-rw-r--r--Hotline/Views/NewsView.swift188
-rw-r--r--Hotline/Views/TrackerView.swift8
-rw-r--r--Hotline/Views/UsersView.swift2
17 files changed, 550 insertions, 146 deletions
diff --git a/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png b/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png
deleted file mode 100644
index c81665b..0000000
--- a/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png
+++ /dev/null
Binary files differ
diff --git a/Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json b/Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json
index 1e3ebc4..fbb9876 100644
--- a/Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,7 +1,7 @@
{
"images" : [
{
- "filename" : "App Icon.png",
+ "filename" : "hotline app icon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
diff --git a/Hotline/Assets.xcassets/AppIcon.appiconset/hotline app icon.png b/Hotline/Assets.xcassets/AppIcon.appiconset/hotline app icon.png
new file mode 100644
index 0000000..57ab755
--- /dev/null
+++ b/Hotline/Assets.xcassets/AppIcon.appiconset/hotline app icon.png
Binary files differ
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift
index a2621b2..533a5fb 100644
--- a/Hotline/Hotline/HotlineClient.swift
+++ b/Hotline/Hotline/HotlineClient.swift
@@ -550,12 +550,17 @@ class HotlineClient {
}
}
- func sendGetNewsCategories(sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsCategory]) -> Void)?) {
- let t = HotlineTransaction(type: .getNewsCategoryNameList)
+ func sendGetNewsCategories(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsCategory]) -> Void)?) {
+ var t = HotlineTransaction(type: .getNewsCategoryNameList)
+ if !path.isEmpty {
+ t.setFieldPath(type: .newsPath, val: path)
+ }
+
self.sendTransaction(t, sent: sent, reply: { rt, err in
var categories: [HotlineNewsCategory] = []
for categoryListItem in rt.getFieldList(type: .newsCategoryListData15) {
- let c = categoryListItem.getNewsCategory()
+ var c = categoryListItem.getNewsCategory()
+ c.path = path + [c.name]
categories.append(c)
print("CATEGORY: \(c)")
}
@@ -565,12 +570,57 @@ class HotlineClient {
})
}
- func sendGetNewsArticles(path: [String]? = nil, sent: ((Bool) -> Void)? = nil, reply: ((String) -> Void)? = nil) {
+ func sendGetNewsArticle(id articleID: UInt32, path: [String], flavor: String, sent: ((Bool) -> Void)? = nil, reply: ((String?) -> Void)? = nil) {
+ var t = HotlineTransaction(type: .getNewsArticleData)
+ t.setFieldPath(type: .newsPath, val: path)
+ t.setFieldUInt32(type: .newsArticleID, val: articleID)
+ t.setFieldString(type: .newsArticleDataFlavor, val: flavor, encoding: .ascii)
+
+ self.sendTransaction(t, sent: sent, reply: { r, err in
+ if err != nil {
+ reply?(nil)
+ return
+ }
+
+ let articleData = r.getField(type: .newsArticleData)
+ let articleString = articleData?.getString()
+
+ print("GOT ARTICLE", articleString)
+
+ reply?(articleString)
+ })
+ }
+
+ func sendGetNewsArticles(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsArticle]) -> Void)? = nil) {
var t = HotlineTransaction(type: .getNewsArticleNameList)
- if path != nil {
- t.setFieldPath(type: .newsPath, val: path!)
+ if !path.isEmpty {
+ t.setFieldPath(type: .newsPath, val: path)
}
- self.sendTransaction(t, sent: sent)
+ self.sendTransaction(t, sent: sent, reply: { r, err in
+ if err != nil {
+ reply?([])
+ return
+ }
+
+ var articles: [HotlineNewsArticle] = []
+ var articleData = r.getField(type: .newsArticleListData)
+
+ print("ARTICLE DATA?", articleData)
+
+ if let newsList = articleData?.getNewsList() {
+ for art in newsList.articles {
+ var blah = art
+ blah.path = path
+ articles.append(blah)
+
+ print(blah.title)
+ }
+ }
+
+ DispatchQueue.main.async {
+ reply?(articles)
+ }
+ })
}
func sendGetFileList(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineFile]) -> Void)? = nil) {
diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift
index 125ec98..f155c0e 100644
--- a/Hotline/Hotline/HotlineProtocol.swift
+++ b/Hotline/Hotline/HotlineProtocol.swift
@@ -63,32 +63,102 @@ struct HotlineServer: Identifiable, Hashable {
}
}
-enum HotlineChatType: Int {
- case message
- case agreement
- case status
+struct HotlineNewsArticle: Identifiable {
+ let id: UInt32
+ let parentID: UInt32
+ let flags: UInt32
+ let title: String
+ let username: String
+ var flavors: [(String, UInt16)] = []
+ var path: [String] = []
+
+ static func == (lhs: HotlineNewsArticle, rhs: HotlineNewsArticle) -> Bool {
+ return lhs.id == rhs.id
+ }
}
-struct HotlineChat: Identifiable {
- let id = UUID()
- let text: String
- let username: String
- let type: HotlineChatType
+struct HotlineNewsList: Identifiable {
+ let id: UInt32
+ let name: String
+ let description: String
+ let count: UInt32
+ var path: [String] = []
- static let parser = /^\s*([^\:]+)\:\s*(.*)/
+ var articles: [HotlineNewsArticle] = []
- init(text: String, type: HotlineChatType = .message) {
- self.type = type
+ init(from data: Data) {
+ self.id = data.readUInt32(at: 0)!
- if
- type == .message,
- let match = text.firstMatch(of: HotlineChat.parser) {
- self.username = String(match.1)
- self.text = String(match.2)
- }
- else {
- self.username = ""
- self.text = text
+ self.count = data.readUInt32(at: 4)!
+
+ let (n, nl) = data.readPString(at: 8)
+ self.name = n!
+
+ let (d, dl) = data.readPString(at: 8 + nl)
+ self.description = d!
+
+ var baseIndex = Int(8 + nl + dl)
+
+ for i in 0..<Int(self.count) {
+ let articleID = data.readUInt32(at: baseIndex)!
+ baseIndex += 4
+
+ let timestampData = data.readData(at: baseIndex, length: 8)!
+ baseIndex += 8
+
+ let parentID = data.readUInt32(at: baseIndex)!
+ baseIndex += 4
+
+ let flags = data.readUInt32(at: baseIndex)!
+ baseIndex += 4
+
+ let flavorCount = data.readUInt16(at: baseIndex)!
+ baseIndex += 2
+
+ let titleLength = data.readUInt8(at: baseIndex)!
+ baseIndex += 1
+
+ let title = data.readString(at: baseIndex, length: Int(titleLength))!
+ baseIndex += Int(titleLength)
+
+ let posterLength = data.readUInt8(at: baseIndex)!
+ baseIndex += 1
+
+ let poster = data.readString(at: baseIndex, length: Int(posterLength))!
+ baseIndex += Int(posterLength)
+
+ var newArticle = HotlineNewsArticle(id: articleID, parentID: parentID, flags: flags, title: title, username: poster)
+
+ print("ARTICLE ID: \(articleID)")
+ print("PARENT ID: \(parentID)")
+ print("FLAGS: \(flags)")
+ print("FLAVOR COUNT: \(flavorCount)")
+ print("TITLE: \(title)")
+ print("POSTER: \(poster)")
+
+ for _ in 0..<Int(flavorCount) {
+ let flavorLength = data.readUInt8(at: baseIndex)!
+ baseIndex += 1
+
+ let flavorText = data.readString(at: baseIndex, length: Int(flavorLength))!
+ baseIndex += Int(flavorLength)
+
+ let articleSize = data.readUInt16(at: baseIndex)!
+ baseIndex += 2
+
+ newArticle.flavors.append((flavorText, articleSize))
+
+
+// let articleString = data.readString(at: baseIndex + i + 4 + 8 + 4 + 4 + 2 + 1 + Int(titleLength) + 1 + Int(posterLength) + 1 + Int(flavorLength) + 2, length: Int(articleSize))!
+
+ print("FLAVOR: \(flavorText)")
+ print("ARTICLE SIZE: \(articleSize)")
+// print("ARTICLE: \(articleString)")
+ }
+
+ self.articles.append(newArticle)
+
+// break
}
}
}
@@ -98,6 +168,7 @@ struct HotlineNewsCategory: Identifiable, Hashable {
let type: UInt16
let count: UInt16
let name: String
+ var path: [String] = []
static func == (lhs: HotlineNewsCategory, rhs: HotlineNewsCategory) -> Bool {
return lhs.id == rhs.id
@@ -384,6 +455,10 @@ struct HotlineTransactionField {
func getNewsCategory() -> HotlineNewsCategory {
return HotlineNewsCategory(from: self.data)
}
+
+ func getNewsList() -> HotlineNewsList {
+ return HotlineNewsList(from: self.data)
+ }
}
struct HotlineTransaction {
diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift
index ed1d154..6c5f01d 100644
--- a/Hotline/Hotline/HotlineTrackerClient.swift
+++ b/Hotline/Hotline/HotlineTrackerClient.swift
@@ -239,7 +239,6 @@ class HotlineTrackerClient {
let ip_4 = self.bytes.readUInt8(at: cursor + 3),
let port = self.bytes.readUInt16(at: cursor + 4),
let userCount = self.bytes.readUInt16(at: cursor + 6) {
-// let nameLengthByte = self.bytes.readUInt8(at: cursor + 10) {
let (serverName, nameByteCount) = self.bytes.readPString(at: cursor + 10)
let (serverDescription, descByteCount) = self.bytes.readPString(at: cursor + 10 + nameByteCount)
@@ -248,7 +247,6 @@ class HotlineTrackerClient {
let desc = serverDescription {
let validName = try? trackerSeparatorRegex.prefixMatch(in: name)
if validName == nil {
-// if name.range(of: regex, options: .regularExpression) == nil {
let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
foundServers.append(server)
}
diff --git a/Hotline/Models/ChatMessage.swift b/Hotline/Models/ChatMessage.swift
index 8a57d61..e585cdf 100644
--- a/Hotline/Models/ChatMessage.swift
+++ b/Hotline/Models/ChatMessage.swift
@@ -15,7 +15,7 @@ struct ChatMessage: Identifiable {
let date: Date
let username: String?
- static let parser = /^\s*([^\:]+)\:\s*(.+)/
+ static let parser = /^\s*([^\:]+):\s*([\s\S]+)$/
init(text: String, type: ChatMessageType, date: Date) {
self.type = type
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index 92366fa..c7474b2 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -6,17 +6,31 @@ import SwiftUI
var status: HotlineClientStatus = .disconnected
- var server: Server? = nil
- var serverVersion: UInt16? = nil
- var serverName: String? = nil
+ var server: Server? {
+ didSet {
+ self.updateServerTitle()
+ }
+ }
+ var serverVersion: UInt16? {
+ didSet {
+ self.updateServerTitle()
+ }
+ }
+ var serverName: String? {
+ didSet {
+ self.updateServerTitle()
+ }
+ }
+ var serverTitle: String = "Server"
var username: String = "bolt"
var iconID: UInt = 128
+ var access: HotlineUserAccessOptions?
var users: [User] = []
var chat: [ChatMessage] = []
var messageBoard: [String] = []
var files: [FileInfo] = []
- var news: [NewsCategory] = []
+ var news: [NewsInfo] = []
// MARK: -
@@ -28,8 +42,8 @@ import SwiftUI
// MARK: -
- @MainActor func getServers(address: String, port: Int = Tracker.defaultPort) async -> [Server] {
- let fetchedServers: [HotlineServer] = await self.trackerClient.fetchServers(address: address, port: port)
+ @MainActor func getServerList(tracker: String, port: Int = Tracker.defaultPort) async -> [Server] {
+ let fetchedServers: [HotlineServer] = await self.trackerClient.fetchServers(address: tracker, port: port)
var servers: [Server] = []
@@ -48,6 +62,7 @@ import SwiftUI
@MainActor func login(server: Server, login: String, password: String, username: String, iconID: UInt) async -> Bool {
self.server = server
+ self.serverName = server.name
self.username = username
self.iconID = iconID
@@ -107,24 +122,164 @@ import SwiftUI
}
}
- @MainActor func getNewsCategories() async -> [NewsCategory] {
+ @MainActor func getNewsArticle(id articleID: UInt, at path: [String], flavor: String) async -> String? {
+ return await withCheckedContinuation { [weak self] continuation in
+ self?.client.sendGetNewsArticle(id: UInt32(articleID), path: path, flavor: flavor, sent: { success in
+ if !success {
+ continuation.resume(returning: nil)
+ return
+ }
+
+ print("GET NEWS CATS FROM \(path)")
+ }, reply: { articleText in
+// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
+
+// var newCategories: [NewsInfo] = []
+// for category in categories {
+// newCategories.append(NewsInfo(hotlineNewsCategory: category))
+// }
+//
+// if let parent = existingNewsItem {
+// parent.children = newCategories
+// }
+// else if path.isEmpty {
+// self?.news = newCategories
+// }
+
+ continuation.resume(returning: articleText)
+ })
+ }
+
+ }
+
+ @MainActor func getNewsList(at path: [String] = []) async -> [NewsInfo] {
return await withCheckedContinuation { [weak self] continuation in
- self?.client.sendGetNewsCategories(sent: { success in
+ var requestCategories = true
+
+ let existingNewsItem = self?.findNews(in: self?.news ?? [], at: path)
+
+ if existingNewsItem != nil {
+ if existingNewsItem!.type != .bundle {
+ requestCategories = false
+ }
+ }
+
+ if requestCategories {
+ self?.client.sendGetNewsCategories(path: path, sent: { success in
+ if !success {
+ continuation.resume(returning: [])
+ return
+ }
+
+ print("GET NEWS CATS FROM \(path)")
+ }, reply: { [weak self] categories in
+// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
+
+ var newCategories: [NewsInfo] = []
+ for category in categories {
+ newCategories.append(NewsInfo(hotlineNewsCategory: category))
+ }
+
+ if let parent = existingNewsItem {
+ parent.children = newCategories
+ }
+ else if path.isEmpty {
+ self?.news = newCategories
+ }
+
+ continuation.resume(returning: newCategories)
+ })
+ }
+ else {
+ self?.client.sendGetNewsArticles(path: path, sent: { success in
+ if !success {
+ continuation.resume(returning: [])
+ return
+ }
+
+ print("GET NEWS ARTICLES FROM \(path)")
+ }, reply: { [weak self] articles in
+// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
+ print("GENERATING NEWS")
+
+ var newArticles: [NewsInfo] = []
+ for article in articles {
+ newArticles.append(NewsInfo(hotlineNewsArticle: article))
+ }
+
+ if let parent = existingNewsItem {
+ print("UNDER PARENT:", parent.name)
+ parent.children = newArticles
+
+ print(parent.children)
+ }
+ else if path.isEmpty {
+ self?.news = newArticles
+ }
+
+ continuation.resume(returning: newArticles)
+ })
+ }
+ }
+ }
+
+ @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] {
+ return await withCheckedContinuation { [weak self] continuation in
+ self?.client.sendGetNewsCategories(path: path, sent: { success in
if !success {
continuation.resume(returning: [])
return
}
+
+ print("GET NEWS CATS FROM \(path)")
}, reply: { [weak self] categories in
- var newCategories: [NewsCategory] = []
+ let parentNews = self?.findNews(in: self?.news ?? [], at: path)
+
+ var newCategories: [NewsInfo] = []
for category in categories {
- newCategories.append(NewsCategory(hotlineNewsCategory: category))
+ newCategories.append(NewsInfo(hotlineNewsCategory: category))
+ }
+
+ if let parent = parentNews {
+ parent.children = newCategories
+ }
+ else if path.isEmpty {
+ self?.news = newCategories
}
- self?.news = newCategories
continuation.resume(returning: newCategories)
})
}
}
+
+ @MainActor func getArticles(at path: [String]) async -> [NewsInfo] {
+ return await withCheckedContinuation { [weak self] continuation in
+ self?.client.sendGetNewsArticles(path: path, sent: { success in
+ if !success {
+ continuation.resume(returning: [])
+ return
+ }
+ }, reply: { [weak self] articles in
+ print("ARTICLES?", articles)
+
+ continuation.resume(returning: [])
+ })
+// 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] {
@@ -143,6 +298,7 @@ import SwiftUI
if status == .disconnected {
self.serverVersion = nil
self.serverName = nil
+ self.access = nil
self.users = []
self.chat = []
self.messageBoard = []
@@ -210,6 +366,8 @@ import SwiftUI
func hotlineReceivedUserAccess(options: HotlineUserAccessOptions) {
print("Hotline: got access options")
print(options, options.contains(.canSendChat), options.contains(.canBroadcast))
+
+ self.access = options
}
func hotlineReceivedError(message: String) {
@@ -218,6 +376,10 @@ import SwiftUI
// MARK: - Utilities
+ func updateServerTitle() {
+ self.serverTitle = self.serverName ?? self.server?.name ?? server?.address ?? "Server"
+ }
+
private func addOrUpdateHotlineUser(_ user: HotlineUser) {
if let i = self.users.firstIndex(where: { $0.id == user.id }) {
print("Hotline: updating user \(self.users[i].name)")
@@ -233,8 +395,6 @@ import SwiftUI
private func findFile(in filesToSearch: [FileInfo], at path: [String]) -> FileInfo? {
guard !path.isEmpty, !filesToSearch.isEmpty else { return nil }
- // var stack: [([HotlineFile], [String])] = [(self.files!, path)]
-
let currentName = path[0]
for file in filesToSearch {
@@ -251,4 +411,24 @@ import SwiftUI
return nil
}
+
+ private func findNews(in newsToSearch: [NewsInfo], at path: [String]) -> NewsInfo? {
+ guard !path.isEmpty, !newsToSearch.isEmpty else { return nil }
+
+ let currentName = path[0]
+
+ for news in newsToSearch {
+ if news.name == currentName {
+ if path.count == 1 {
+ return news
+ }
+ else if !news.children.isEmpty {
+ let remainingPath = Array(path[1...])
+ return self.findNews(in: news.children, at: remainingPath)
+ }
+ }
+ }
+
+ return nil
+ }
}
diff --git a/Hotline/Models/NewsCategory.swift b/Hotline/Models/NewsCategory.swift
deleted file mode 100644
index 0ba87a7..0000000
--- a/Hotline/Models/NewsCategory.swift
+++ /dev/null
@@ -1,34 +0,0 @@
-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
- }
-}
diff --git a/Hotline/Models/NewsInfo.swift b/Hotline/Models/NewsInfo.swift
new file mode 100644
index 0000000..ad6bfcd
--- /dev/null
+++ b/Hotline/Models/NewsInfo.swift
@@ -0,0 +1,58 @@
+import SwiftUI
+
+enum NewsInfoType {
+ case bundle
+ case category
+ case article
+}
+
+@Observable class NewsInfo: Identifiable, Hashable {
+ let id: UUID = UUID()
+
+ let name: String
+ let count: UInt
+ let type: NewsInfoType
+
+ let categoryID: UUID?
+ let articleID: UInt?
+
+ let path: [String]
+ var children: [NewsInfo] = []
+
+ var articleFlavors: [String]?
+
+ init(hotlineNewsCategory: HotlineNewsCategory) {
+ self.categoryID = hotlineNewsCategory.id
+ self.articleID = nil
+ self.name = hotlineNewsCategory.name
+ self.count = UInt(hotlineNewsCategory.count)
+ self.path = hotlineNewsCategory.path
+
+ if hotlineNewsCategory.type == 2 {
+ self.type = .bundle
+ }
+ else {
+ self.type = .category
+ }
+ }
+
+ init(hotlineNewsArticle: HotlineNewsArticle) {
+ self.articleID = UInt(hotlineNewsArticle.id)
+ self.categoryID = nil
+ self.name = hotlineNewsArticle.title
+ self.count = 0
+// self.count = UInt(hotlineNewsArticle.count)
+ self.path = hotlineNewsArticle.path
+ self.type = .article
+
+ self.articleFlavors = hotlineNewsArticle.flavors.map { $0.0 }
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(self.id)
+ }
+
+ static func == (lhs: NewsInfo, rhs: NewsInfo) -> Bool {
+ return lhs.id == rhs.id
+ }
+}
diff --git a/Hotline/Models/Server.swift b/Hotline/Models/Server.swift
index b159a1a..8b5ea56 100644
--- a/Hotline/Models/Server.swift
+++ b/Hotline/Models/Server.swift
@@ -4,13 +4,13 @@ import SwiftUI
static let defaultPort: Int = 5500
let id: UUID = UUID()
- let name: String
+ let name: String?
let description: String?
let users: Int
let address: String
let port: Int
- init(name: String, description: String?, address: String, port: Int, users: Int = 0) {
+ init(name: String?, description: String?, address: String, port: Int, users: Int = 0) {
self.name = name
self.description = description
self.address = address
diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift
index 8e5b135..e8c7754 100644
--- a/Hotline/Views/ChatView.swift
+++ b/Hotline/Views/ChatView.swift
@@ -32,7 +32,7 @@ struct ChatView: View {
.opacity(0.75)
HStack {
Spacer()
- Text((model.server?.name ?? "") + " Server Agreement")
+ Text((model.serverTitle) + " Server Agreement")
.font(.caption)
.fontWeight(.medium)
.opacity(0.4)
@@ -78,6 +78,7 @@ struct ChatView: View {
}
.padding(.bottom, 12)
}
+ .defaultScrollAnchor(.bottom)
.onChange(of: model.chat.count) {
withAnimation {
reader.scrollTo(bottomID, anchor: .bottom)
@@ -127,7 +128,7 @@ struct ChatView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
- Text(model.server?.name ?? "")
+ Text(model.serverTitle)
.font(.headline)
}
ToolbarItem(placement: .navigationBarLeading) {
diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift
index 5cbf0c9..8ad3837 100644
--- a/Hotline/Views/FilesView.swift
+++ b/Hotline/Views/FilesView.swift
@@ -116,7 +116,7 @@ struct FilesView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
- Text(model.server?.name ?? "")
+ Text(model.serverTitle)
.font(.headline)
}
ToolbarItem(placement: .navigationBarLeading) {
diff --git a/Hotline/Views/MessageBoardView.swift b/Hotline/Views/MessageBoardView.swift
index 8a1c831..0d3968f 100644
--- a/Hotline/Views/MessageBoardView.swift
+++ b/Hotline/Views/MessageBoardView.swift
@@ -42,7 +42,7 @@ struct MessageBoardView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
- Text(model.server?.name ?? "")
+ Text(model.serverTitle)
.font(.headline)
}
ToolbarItem(placement: .navigationBarLeading) {
diff --git a/Hotline/Views/NewsView.swift b/Hotline/Views/NewsView.swift
index ad7904e..bd3ae1c 100644
--- a/Hotline/Views/NewsView.swift
+++ b/Hotline/Views/NewsView.swift
@@ -1,58 +1,142 @@
import SwiftUI
+import UniformTypeIdentifiers
-struct NewsView: View {
+
+
+struct NewsItemView: View {
@Environment(Hotline.self) private var model: Hotline
- @Environment(\.colorScheme) var colorScheme
+ @Environment(NewsItemSelection.self) private var selectedArticle: NewsItemSelection
- @State private var fetched = false
- @State private var selectedCategory: NewsCategory? = nil
- @State private var topListHeight: CGFloat = 200
- @State private var dividerHeight: CGFloat = 30
+ let news: NewsInfo
- var articleList: some View {
-
- // Your list content goes here
- List {
- ForEach(model.news, id: \.self) { category in
- DisclosureGroup {
- ProgressView(value: 0.4)
- .task {
- print("EXPANDED?", category.name)
-// hotline.sendGetNewsArticles(path: [cat.name]) {
-// print("OK")
-// }
- }
- } label: {
- Text(category.name)
+ @State var expanded = false
+
+ var body: some View {
+ if news.count > 0 {
+ DisclosureGroup(isExpanded: $expanded) {
+ ForEach(news.children) { childNews in
+ NewsItemView(news: childNews)
+ .environment(self.selectedArticle)
+ .frame(height: 38)
+ }
+ } label: {
+ HStack {
+ if news.type == .bundle {
+ Text(Image(systemName: "tray.2.fill"))
+ }
+ else {
+ Text(Image(systemName: "tray.full.fill"))
+ }
+ Text(news.name)
.fontWeight(.medium)
.lineLimit(1)
.truncationMode(.tail)
+ Spacer()
+ if news.count > 0 {
+ Text("\(news.count)")
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ .onChange(of: expanded) {
+ if !expanded {
+ return
+ }
+
+ Task {
+ await model.getNewsList(at: news.path)
}
}
}
- .scrollBounceBehavior(.basedOnSize)
-// .listStyle(.plain)
- }
-
- var readerView: some View {
- // Your list content goes here
- ScrollView(.vertical) {
- HStack(alignment: .top, spacing: 0) {
- Text("HELLO")
- .multilineTextAlignment(.leading)
+ else {
+ HStack {
+ Text(Image(systemName: "doc.text"))
+ Text(news.name)
+ .fontWeight(.medium)
+ .lineLimit(1)
+ .truncationMode(.tail)
Spacer()
+ if news.count > 0 {
+ Text("\(news.count)")
+ .foregroundStyle(.secondary)
+ }
+ }
+ .onTapGesture {
+ if news.type == .article {
+ print("SELECTED", news.name)
+ selectedArticle.selectedArticle = news
+ }
}
- .padding()
}
- .scrollBounceBehavior(.basedOnSize)
- .background(colorScheme == .dark ? Color(white: 0.1) : Color(uiColor: UIColor.systemBackground))
}
+}
+
+@Observable
+class NewsItemSelection: Equatable {
+ var selectedArticle: NewsInfo? = nil
+ static func == (lhs: NewsItemSelection, rhs: NewsItemSelection) -> Bool {
+ return lhs.selectedArticle == rhs.selectedArticle
+ }
+}
+
+struct NewsView: View {
+ @Environment(Hotline.self) private var model: Hotline
+ @Environment(\.colorScheme) var colorScheme
+
+ @State private var fetched = false
+ @State private var selectedCategory: NewsInfo? = nil
+ @State private var topListHeight: CGFloat = 200
+ @State private var dividerHeight: CGFloat = 30
+
+ @State private var articleSelection = NewsItemSelection()
+ @State private var articleText = ""
+// @State private var selectedArticleID: UInt?
+
+ var articleList: some View {
+ VStack(spacing: 0) {
+ if model.news.count == 0 {
+ Text("No News Available")
+ .font(.headline)
+ .opacity(0.3)
+ }
+ else {
+ List(model.news) { category in
+ NewsItemView(news: category)
+ .environment(self.articleSelection)
+ .frame(height: 38)
+ }
+ .scrollBounceBehavior(.basedOnSize)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color(uiColor: .systemGroupedBackground))
+
+ // .listStyle(.plain)
+ }
+
var body: some View {
NavigationStack {
VStack(spacing: 0) {
articleList
.frame(height: topListHeight)
+ .frame(minHeight: topListHeight)
+ .onChange(of: self.articleSelection.selectedArticle) {
+ self.articleText = ""
+ if
+ let article = self.articleSelection.selectedArticle,
+ let articleFlavor = article.articleFlavors?.first,
+ let articleID = article.articleID {
+ Task {
+ if let articleText = await self.model.getNewsArticle(id: articleID, at: article.path, flavor: articleFlavor) {
+ self.articleText = articleText
+ }
+ }
+ }
+// print("SELECTED ARTICLE", articleSelection.selectedArticle?.name)
+ }
+
+ // Movable Divider
VStack(alignment: .center) {
Divider()
Spacer()
@@ -72,28 +156,31 @@ struct NewsView: View {
.onChanged { gesture in
let delta = gesture.translation.height
topListHeight = max(min(topListHeight + delta, 500), 50)
- // bottomListHeight = max(min(bottomListHeight - delta, 400), 0)
}
)
- readerView
+
+ // Reader View
+ ScrollView(.vertical) {
+ HStack(alignment: .top, spacing: 0) {
+ Text(self.articleText)
+ .multilineTextAlignment(.leading)
+ Spacer()
+ }
+ .padding()
+ }
+ .scrollBounceBehavior(.basedOnSize)
+ .background(colorScheme == .dark ? Color(white: 0.1) : Color(uiColor: UIColor.systemBackground))
}
.task {
if !fetched {
- let _ = await model.getNewsCategories()
+ let _ = await model.getNewsList()
fetched = true
-
- // hotline.sendGetNewsArticles(path: ["News"]) {
- // print("GOT ARTICLES?")
- // }
}
}
- // .refreshable {
- // hotline.sendGetNewsCategories()
- // }
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
- Text(model.server?.name ?? "")
+ Text(model.serverTitle)
.font(.headline)
}
ToolbarItem(placement: .navigationBarLeading) {
@@ -106,19 +193,8 @@ struct NewsView: View {
.foregroundColor(.secondary)
}
}
-// ToolbarItem(placement: .navigationBarTrailing) {
-// Button {
-//
-// } label: {
-// Image(systemName: "square.and.pencil")
-// // .symbolRenderingMode(.hierarchical)
-// // .foregroundColor(.secondary)
-// }
-//
-// }
}
}
-
}
}
diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift
index acb2069..6204fd2 100644
--- a/Hotline/Views/TrackerView.swift
+++ b/Hotline/Views/TrackerView.swift
@@ -86,7 +86,7 @@ struct TrackerConnectView: View {
)
.cornerRadius(10.0)
Button {
- let s = Server(name: address, description: nil, address: address, port: Server.defaultPort, users: 0)
+ let s = Server(name: nil, description: nil, address: address, port: Server.defaultPort, users: 0)
server = s
connecting = true
Task {
@@ -200,7 +200,7 @@ struct TrackerView: View {
// "tracked.nailbat.com"
// "hotline.duckdns.org"
// "tracked.agent79.org"
- self.servers = await model.getServers(address: "hltracker.com")
+ self.servers = await model.getServerList(tracker: "hltracker.com")
}
var body: some View {
@@ -267,7 +267,7 @@ struct TrackerView: View {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "globe.americas.fill").font(.title3)
VStack(alignment: .leading) {
- Text(server.name).font(.title3).fontWeight(.medium)
+ Text(server.name ?? server.address).font(.title3).fontWeight(.medium)
if shouldDisplayDescription(server: server) {
Spacer()
Text(server.description!).opacity(0.5).font(.system(size: 16))
@@ -382,7 +382,7 @@ struct TrackerView: View {
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)
+ let _ = await model.login(server: Server(name: nil, 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.
diff --git a/Hotline/Views/UsersView.swift b/Hotline/Views/UsersView.swift
index f923ff4..d1f3318 100644
--- a/Hotline/Views/UsersView.swift
+++ b/Hotline/Views/UsersView.swift
@@ -17,7 +17,7 @@ struct UsersView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
- Text(model.server?.name ?? "")
+ Text(model.serverTitle)
.font(.headline)
}
ToolbarItem(placement: .navigationBarLeading) {