aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2024-05-03 12:49:52 -0700
committerDustin Mierau <dustin@mierau.me>2024-05-03 12:49:52 -0700
commit02b452ccaf54cb24eb38fcb752126888994c0a1b (patch)
treec30e71a1df363092a369fd7847c6223914662ce4 /Hotline
parent73208d70d21db7e526e371fa863acd0327caad9e (diff)
First pass at private messages UI.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Assets.xcassets/Incoming Message Background.colorset/Contents.json38
-rw-r--r--Hotline/Assets.xcassets/Incoming Message Text.colorset/Contents.json38
-rw-r--r--Hotline/Assets.xcassets/Link Color.colorset/Contents.json6
-rw-r--r--Hotline/Assets.xcassets/Outgoing Message Background.colorset/Contents.json38
-rw-r--r--Hotline/Assets.xcassets/Outgoing Message Link.colorset/Contents.json38
-rw-r--r--Hotline/Assets.xcassets/Outgoing Message Text.colorset/Contents.json38
-rw-r--r--Hotline/Assets.xcassets/Unread Message Dot.colorset/Contents.json38
-rw-r--r--Hotline/Hotline/HotlineClient.swift24
-rw-r--r--Hotline/Models/Hotline.swift66
-rw-r--r--Hotline/Models/InstantMessage.swift18
-rw-r--r--Hotline/Models/User.swift6
-rw-r--r--Hotline/Utility/FoundationExtensions.swift1
-rw-r--r--Hotline/Utility/SoundEffects.swift1
-rw-r--r--Hotline/macOS/ChatView.swift23
-rw-r--r--Hotline/macOS/MessageView.swift166
-rw-r--r--Hotline/macOS/ServerView.swift26
16 files changed, 439 insertions, 126 deletions
diff --git a/Hotline/Assets.xcassets/Incoming Message Background.colorset/Contents.json b/Hotline/Assets.xcassets/Incoming Message Background.colorset/Contents.json
new file mode 100644
index 0000000..c5fd6a0
--- /dev/null
+++ b/Hotline/Assets.xcassets/Incoming Message Background.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xF8",
+ "green" : "0xF8",
+ "red" : "0xF8"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "0.150",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFF"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Hotline/Assets.xcassets/Incoming Message Text.colorset/Contents.json b/Hotline/Assets.xcassets/Incoming Message Text.colorset/Contents.json
new file mode 100644
index 0000000..0c600f9
--- /dev/null
+++ b/Hotline/Assets.xcassets/Incoming Message Text.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFF"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Hotline/Assets.xcassets/Link Color.colorset/Contents.json b/Hotline/Assets.xcassets/Link Color.colorset/Contents.json
index e1d527b..c4149fb 100644
--- a/Hotline/Assets.xcassets/Link Color.colorset/Contents.json
+++ b/Hotline/Assets.xcassets/Link Color.colorset/Contents.json
@@ -6,7 +6,7 @@
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
- "green" : "0x86",
+ "green" : "0x78",
"red" : "0x1F"
}
},
@@ -24,8 +24,8 @@
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
- "green" : "0xB0",
- "red" : "0x6C"
+ "green" : "0xAA",
+ "red" : "0x71"
}
},
"idiom" : "universal"
diff --git a/Hotline/Assets.xcassets/Outgoing Message Background.colorset/Contents.json b/Hotline/Assets.xcassets/Outgoing Message Background.colorset/Contents.json
new file mode 100644
index 0000000..9a51a31
--- /dev/null
+++ b/Hotline/Assets.xcassets/Outgoing Message Background.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "0.800",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "0.800",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Hotline/Assets.xcassets/Outgoing Message Link.colorset/Contents.json b/Hotline/Assets.xcassets/Outgoing Message Link.colorset/Contents.json
new file mode 100644
index 0000000..51c9937
--- /dev/null
+++ b/Hotline/Assets.xcassets/Outgoing Message Link.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xAA",
+ "red" : "0x71"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xAA",
+ "red" : "0x71"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Hotline/Assets.xcassets/Outgoing Message Text.colorset/Contents.json b/Hotline/Assets.xcassets/Outgoing Message Text.colorset/Contents.json
new file mode 100644
index 0000000..2536dc2
--- /dev/null
+++ b/Hotline/Assets.xcassets/Outgoing Message Text.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFF"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFF"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Hotline/Assets.xcassets/Unread Message Dot.colorset/Contents.json b/Hotline/Assets.xcassets/Unread Message Dot.colorset/Contents.json
new file mode 100644
index 0000000..05fcb96
--- /dev/null
+++ b/Hotline/Assets.xcassets/Unread Message Dot.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0xF1"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0xF1"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift
index 5ed71a3..f0e5eea 100644
--- a/Hotline/Hotline/HotlineClient.swift
+++ b/Hotline/Hotline/HotlineClient.swift
@@ -37,6 +37,7 @@ protocol HotlineClientDelegate: AnyObject {
func hotlineReceivedChatMessage(message: String)
func hotlineReceivedUserList(users: [HotlineUser])
func hotlineReceivedServerMessage(message: String)
+ func hotlineReceivedPrivateMessage(userID: UInt16, message: String)
func hotlineReceivedUserAccess(options: HotlineUserAccessOptions)
func hotlineUserChanged(user: HotlineUser)
func hotlineUserDisconnected(userID: UInt16)
@@ -49,6 +50,7 @@ extension HotlineClientDelegate {
func hotlineReceivedChatMessage(message: String) {}
func hotlineReceivedUserList(users: [HotlineUser]) {}
func hotlineReceivedServerMessage(message: String) {}
+ func hotlineReceivedPrivateMessage(userID: UInt16, message: String) {}
func hotlineReceivedUserAccess(options: HotlineUserAccessOptions) {}
func hotlineUserChanged(user: HotlineUser) {}
func hotlineUserDisconnected(userID: UInt16) {}
@@ -303,11 +305,12 @@ class HotlineClient: NetSocketDelegate {
let userIconID = userIconIDField.getUInt16(),
let userFlagsField = packet.getField(type: .userFlags),
let userFlags = userFlagsField.getUInt16() {
- print("HotlineClient: User changed \(username) icon: \(userIconID)")
+ print("HotlineClient: User changed \(userID) \(username) icon: \(userIconID)")
let user = HotlineUser(id: userID, iconID: userIconID, status: userFlags, name: username)
self.delegate?.hotlineUserChanged(user: user)
}
+
case .notifyOfUserDelete:
if let userIDField = packet.getField(type: .userID),
let userID = userIDField.getUInt16() {
@@ -322,7 +325,14 @@ class HotlineClient: NetSocketDelegate {
case .serverMessage:
if let messageField = packet.getField(type: .data),
let message = messageField.getString() {
- self.delegate?.hotlineReceivedServerMessage(message: message)
+
+ if let userIDField = packet.getField(type: .userID),
+ let userID = userIDField.getUInt16() {
+ self.delegate?.hotlineReceivedPrivateMessage(userID: userID, message: message)
+ }
+ else {
+ self.delegate?.hotlineReceivedServerMessage(message: message)
+ }
}
case .showAgreement:
@@ -448,6 +458,14 @@ class HotlineClient: NetSocketDelegate {
self.sendPacket(t)
}
+ @MainActor func sendInstantMessage(message: String, userID: UInt16, encoding: String.Encoding = .utf8) {
+ var t = HotlineTransaction(type: .sendInstantMessage)
+ t.setFieldUInt16(type: .userID, val: userID)
+ t.setFieldUInt32(type: .options, val: 1)
+ t.setFieldString(type: .data, val: message, encoding: encoding)
+ self.sendPacket(t)
+ }
+
@MainActor func sendGetUserList() {
let t = HotlineTransaction(type: .getUserNameList)
self.sendPacket(t) { [weak self] reply, err in
@@ -626,7 +644,7 @@ class HotlineClient: NetSocketDelegate {
let fileName = reply.getField(type: .fileName)?.getString(),
let fileCreator = reply.getField(type: .fileCreatorString)?.getString(),
let fileType = reply.getField(type: .fileTypeString)?.getString(),
- let fileTypeString = reply.getField(type: .fileTypeString)?.getString(),
+ let _ = reply.getField(type: .fileTypeString)?.getString(),
let fileCreateDate = reply.getField(type: .fileCreateDate)?.data.readDate(at: 0),
let fileModifyDate = reply.getField(type: .fileModifyDate)?.data.readDate(at: 0)
else {
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index 95a21c6..beed6a3 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -108,7 +108,6 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
]
var status: HotlineClientStatus = .disconnected
-
var server: Server? {
didSet {
self.updateServerTitle()
@@ -125,8 +124,6 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
var iconID: Int = 414
var access: HotlineUserAccessOptions?
var agreed: Bool = false
-
- var currentUser: User? = nil
var users: [User] = []
var chat: [ChatMessage] = []
var messageBoard: [String] = []
@@ -135,9 +132,10 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
var filesLoaded: Bool = false
var news: [NewsInfo] = []
var newsLoaded: Bool = false
-
+ var instantMessages: [UInt16:[InstantMessage]] = [:]
var transfers: [TransferInfo] = []
var downloads: [HotlineFileClient] = []
+ var unreadInstantMessages: [UInt16:UInt16] = [:]
@ObservationIgnored var bannerClient: HotlineFileClient?
#if os(macOS)
@@ -211,6 +209,31 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
self.client.sendAgree(username: self.username, iconID: UInt16(self.iconID), options: .none)
}
+ @MainActor func sendInstantMessage(_ text: String, userID: UInt16) {
+ let message = InstantMessage(direction: .outgoing, text: text, type: .message, date: Date())
+
+ if self.instantMessages[userID] == nil {
+ self.instantMessages[userID] = [message]
+ }
+ else {
+ self.instantMessages[userID]!.append(message)
+ }
+
+ self.client.sendInstantMessage(message: text, userID: userID)
+
+ if Prefs.shared.playPrivateMessageSound && Prefs.shared.playPrivateMessageSound {
+ SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
+ }
+ }
+
+ func hasUnreadInstantMessages(userID: UInt16) -> Bool {
+ return self.unreadInstantMessages[userID] != nil
+ }
+
+ func markInstantMessagesAsRead(userID: UInt16) {
+ self.unreadInstantMessages.removeValue(forKey: userID)
+ }
+
@MainActor func sendChat(_ text: String) {
self.client.sendChat(message: text)
}
@@ -681,19 +704,43 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
}
func hotlineReceivedServerMessage(message: String) {
-// print("Hotline: received server message:\n\(message)")
-// self.chat.append(ChatMessage(text: message, type: .server, date: Date()))
+ if Prefs.shared.playChatSound && Prefs.shared.playChatSound {
+ SoundEffectPlayer.shared.playSoundEffect(.serverMessage)
+ }
+
+ print("Hotline: received server message:\n\(message)")
+ self.chat.append(ChatMessage(text: message, type: .server, date: Date()))
+ }
+
+ func hotlineReceivedPrivateMessage(userID: UInt16, message: String) {
+ if let existingUserIndex = self.users.firstIndex(where: { $0.id == UInt(userID) }) {
+ let user = self.users[existingUserIndex]
+ print("Hotline: received private message from \(user.name): \(message)")
+
+ if Prefs.shared.playPrivateMessageSound && Prefs.shared.playPrivateMessageSound {
+ SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
+ }
+
+ let instantMessage = InstantMessage(direction: .incoming, text: message, type: .message, date: Date())
+ if self.instantMessages[userID] == nil {
+ self.instantMessages[userID] = [instantMessage]
+ }
+ else {
+ self.instantMessages[userID]!.append(instantMessage)
+ }
+ self.unreadInstantMessages[userID] = userID
+ }
}
func hotlineReceivedChatMessage(message: String) {
- if Prefs().playSounds && Prefs().playChatSound {
+ if Prefs.shared.playSounds && Prefs.shared.playChatSound {
SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
}
self.chat.append(ChatMessage(text: message, type: .message, date: Date()))
}
func hotlineReceivedUserList(users: [HotlineUser]) {
- var existingUserIDs: [UInt] = []
+ var existingUserIDs: [UInt16] = []
var userList: [User] = []
for u in users {
@@ -701,7 +748,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
// If a user is already in the user list we have to assume
// they changed somehow before we received the user list
// which means let's keep their existing info.
- existingUserIDs.append(UInt(u.id))
+ existingUserIDs.append(u.id)
userList.append(self.users[i])
}
else {
@@ -827,6 +874,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
}
private func addOrUpdateHotlineUser(_ user: HotlineUser) {
+ print("Hotline: users: \n\(self.users)")
if let i = self.users.firstIndex(where: { $0.id == user.id }) {
print("Hotline: updating user \(self.users[i].name)")
self.users[i] = User(hotlineUser: user)
diff --git a/Hotline/Models/InstantMessage.swift b/Hotline/Models/InstantMessage.swift
new file mode 100644
index 0000000..c41e33b
--- /dev/null
+++ b/Hotline/Models/InstantMessage.swift
@@ -0,0 +1,18 @@
+import SwiftUI
+
+enum InstantMessageType {
+ case message
+}
+
+enum InstantMessageDirection {
+ case incoming
+ case outgoing
+}
+
+struct InstantMessage: Identifiable {
+ let id = UUID()
+ let direction: InstantMessageDirection
+ let text: String
+ let type: InstantMessageType
+ let date: Date
+}
diff --git a/Hotline/Models/User.swift b/Hotline/Models/User.swift
index 7fbecb0..21a8fff 100644
--- a/Hotline/Models/User.swift
+++ b/Hotline/Models/User.swift
@@ -8,7 +8,7 @@ struct UserStatus: OptionSet {
}
struct User: Identifiable {
- var id: UInt
+ var id: UInt16
var name: String
var iconID: UInt
var status: UserStatus
@@ -21,13 +21,13 @@ struct User: Identifiable {
if hotlineUser.isIdle { status.update(with: .idle) }
if hotlineUser.isAdmin { status.update(with: .admin) }
- self.id = UInt(hotlineUser.id)
+ self.id = hotlineUser.id
self.name = hotlineUser.name
self.iconID = UInt(hotlineUser.iconID)
self.status = status
}
- init(id: UInt, name: String, iconID: UInt, status: UserStatus) {
+ init(id: UInt16, name: String, iconID: UInt, status: UserStatus) {
self.id = id
self.name = name
self.iconID = iconID
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index 9ad0d8b..c154dbd 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -17,7 +17,6 @@ extension String {
}
regex.enumerateMatches(in: self, range: NSMakeRange(0, self.count)) { (result: NSTextCheckingResult!, _, _) in
- let range = result.range
if let newRange = Range(result.range, in: self) {
let str = String(self[newRange])
attributedString.addAttribute(.link, value: str, range: result.range)
diff --git a/Hotline/Utility/SoundEffects.swift b/Hotline/Utility/SoundEffects.swift
index 931e27f..629af71 100644
--- a/Hotline/Utility/SoundEffects.swift
+++ b/Hotline/Utility/SoundEffects.swift
@@ -8,6 +8,7 @@ enum SoundEffects: String {
case userLogin = "user-login"
case userLogout = "user-logout"
case newNews = "new-news"
+ case serverMessage = "server-message"
}
@Observable
diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift
index 35ff28e..951544f 100644
--- a/Hotline/macOS/ChatView.swift
+++ b/Hotline/macOS/ChatView.swift
@@ -24,7 +24,6 @@ struct ChatView: View {
var body: some View {
NavigationStack {
ScrollViewReader { reader in
-
VStack(alignment: .leading, spacing: 0) {
// MARK: Scroll View
@@ -193,17 +192,17 @@ struct ChatView: View {
}
}
.background(Color(nsColor: .textBackgroundColor))
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button {
- if prepareChatDocument() {
- showingExporter = true
- }
- } label: {
- Image(systemName: "square.and.arrow.up")
- }.help("Save Chat...")
- }
- }
+// .toolbar {
+// ToolbarItem(placement: .primaryAction) {
+// Button {
+// if prepareChatDocument() {
+// showingExporter = true
+// }
+// } label: {
+// Image(systemName: "square.and.arrow.up")
+// }.help("Save Chat...")
+// }
+// }
.fileExporter(isPresented: $showingExporter, document: self.chatDocument, contentType: .utf8PlainText, defaultFilename: "\(self.model.serverTitle) Chat.txt") { result in
switch result {
case .success(let url):
diff --git a/Hotline/macOS/MessageView.swift b/Hotline/macOS/MessageView.swift
index e880435..94d3d2e 100644
--- a/Hotline/macOS/MessageView.swift
+++ b/Hotline/macOS/MessageView.swift
@@ -1,130 +1,108 @@
import SwiftUI
-//extension View {
-// func endEditing() {
-// UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
-// }
-//}
-
struct MessageView: View {
@Environment(Hotline.self) private var model: Hotline
- @Environment(\.colorScheme) var colorScheme
+ @Environment(\.colorScheme) private var colorScheme
- @State var input: String = ""
+ @State private var input: String = ""
@State private var scrollPos: Int?
@State private var contentHeight: CGFloat = 0
+ @Namespace private var bottomID
+ @FocusState private var focusedField: FocusedField?
- @Namespace var bottomID
-
- let userID: UInt
-
+ var userID: UInt16
+
var body: some View {
- NavigationStack {
- VStack(spacing: 0) {
- ScrollViewReader { reader in
- ScrollView {
+ ScrollViewReader { reader in
+ VStack(alignment: .leading, spacing: 0) {
+
+ // MARK: Scroll View
+ GeometryReader { gm in
+ ScrollView(.vertical) {
LazyVStack(alignment: .leading) {
-// ForEach(model.chat) { msg in
-// if msg.type == .agreement {
-// VStack(alignment: .leading) {
-// VStack(alignment: .leading, spacing: 0) {
-// Text(msg.text)
-// .textSelection(.enabled)
-// .padding()
-// .opacity(0.75)
-// HStack {
-// Spacer()
-// Text((model.serverTitle) + " Server Agreement")
-// .font(.caption)
-// .fontWeight(.medium)
-// .opacity(0.4)
-// .lineLimit(1)
-// .truncationMode(.middle)
-// Spacer()
-// }
-// .padding()
-// .background(colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.9))
-// }
-// .background(colorScheme == .dark ? Color(white: 0.1) : Color(white: 0.96))
-// .cornerRadius(16)
-// .frame(maxWidth: .infinity)
-// }
-// .padding()
-// }
-// else if msg.type == .status {
-// HStack {
-// Spacer()
-// Text(msg.text)
-// .lineLimit(1)
-// .truncationMode(.middle)
-// .opacity(0.3)
-// Spacer()
-// }
-// .padding()
-// }
-// else {
-// HStack(alignment: .firstTextBaseline) {
-// if let username = msg.username {
-// Text("**\(username):** \(msg.text)")
-// }
-// else {
-// Text(msg.text)
-// .textSelection(.enabled)
-// }
-// Spacer()
-// }
-// .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
-// }
-// }
+ ForEach(model.instantMessages[userID] ?? [InstantMessage]()) { msg in
+ HStack(alignment: .firstTextBaseline) {
+ if msg.direction == .outgoing {
+ Spacer()
+ }
+
+ Text(LocalizedStringKey(msg.text.convertLinksToMarkdown()))
+ .lineSpacing(4)
+ .multilineTextAlignment(.leading)
+ .textSelection(.enabled)
+ .tint(msg.direction == .outgoing ? Color("Outgoing Message Link") : Color("Link Color"))
+ .foregroundStyle(msg.direction == .outgoing ? Color("Outgoing Message Text") : Color("Incoming Message Text"))
+ .padding(EdgeInsets(top: 10, leading: 14, bottom: 10, trailing: 14))
+ .background(msg.direction == .outgoing ? Color("Outgoing Message Background") : Color("Incoming Message Background"))
+ .clipShape(RoundedRectangle(cornerRadius: 16))
+
+ if msg.direction == .incoming {
+ Spacer()
+ }
+ }
+ .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
+ }
+
EmptyView().id(bottomID)
}
- .padding(.bottom, 12)
+ .padding()
}
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
.defaultScrollAnchor(.bottom)
- .onChange(of: model.chat.count) {
- withAnimation {
+ .onChange(of: model.instantMessages[userID]?.count) {
+ withAnimation(.easeOut(duration: 0.15).delay(0.25)) {
reader.scrollTo(bottomID, anchor: .bottom)
}
- print("SCROLLED TO BOTTOM")
+ model.markInstantMessagesAsRead(userID: userID)
}
.onAppear {
reader.scrollTo(bottomID, anchor: .bottom)
+ model.markInstantMessagesAsRead(userID: userID)
}
- .scrollDismissesKeyboard(.interactively)
- .onTapGesture {
-// self.endEditing()
+ .onChange(of: gm.size) {
+ reader.scrollTo(bottomID, anchor: .bottom)
}
- .textSelection(.enabled)
}
+ // MARK: Input Divider
Divider()
- HStack(alignment: .top) {
- Image(systemName: "chevron.right").opacity(0.4)
- TextField("", text: $input, axis: .vertical)
+ // MARK: Input Bar
+ HStack(alignment: .lastTextBaseline, spacing: 0) {
+ let user = model.users.first(where: { $0.id == userID })
+ TextField("Message \(user?.name ?? "")", text: $input, axis: .vertical)
+ .focused($focusedField, equals: .chatInput)
.textFieldStyle(.plain)
.lineLimit(1...5)
+ .multilineTextAlignment(.leading)
.onSubmit {
-// if !self.input.isEmpty {
-// model.sendChat(self.input)
-// }
+ if !self.input.isEmpty {
+ model.sendInstantMessage(self.input, userID: self.userID)
+ }
self.input = ""
}
.frame(maxWidth: .infinity)
- Button {
-// if !self.input.isEmpty {
-// model.sendChat(self.input)
-// }
- self.input = ""
- } label: {
- Image(systemName: self.input.isEmpty ? "arrow.up.circle" : "arrow.up.circle.fill")
- .resizable()
- .scaledToFit()
- .frame(width: 24.0, height: 24.0)
- .opacity(self.input.isEmpty ? 0.4 : 1.0)
+ .padding()
+ }
+ .frame(maxWidth: .infinity, minHeight: 28)
+ .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
+ .overlay(alignment: .leadingFirstTextBaseline) {
+ Image(systemName: "chevron.right").opacity(0.4).offset(x: 16)
+ }
+ .onContinuousHover { phase in
+ switch phase {
+ case .active(_):
+ NSCursor.iBeam.set()
+ case .ended:
+ NSCursor.arrow.set()
+ break
}
- }.padding()
+ }
+ .onTapGesture {
+ focusedField = .chatInput
+ }
}
+ .background(Color(nsColor: .textBackgroundColor))
}
}
}
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index c34fa68..b3ab302 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -115,7 +115,7 @@ enum ServerNavigationType: Identifiable, Hashable, Equatable {
case news
case board
case files
- case user(userID: UInt)
+ case user(userID: UInt16)
}
struct ServerView: View {
@@ -351,6 +351,14 @@ struct ServerView: View {
if model.users.count > 0 {
self.usersSection
+ .onChange(of: state.selection) {
+ switch(state.selection) {
+ case .user(let userID):
+ model.markInstantMessagesAsRead(userID: userID)
+ default:
+ break
+ }
+ }
}
}
}
@@ -382,6 +390,13 @@ struct ServerView: View {
.foregroundStyle(user.isAdmin ? Color(hex: 0xE10000) : .primary)
Spacer()
+
+ if model.hasUnreadInstantMessages(userID: user.id) {
+ Circle()
+ .frame(width: 6, height: 6)
+ .padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 2))
+ .opacity(0.5)
+ }
}
.opacity(user.isIdle ? 0.6 : 1.0)
.opacity(controlActiveState == .inactive ? 0.4 : 1.0)
@@ -400,23 +415,32 @@ struct ServerView: View {
case .chat:
ChatView()
.navigationTitle(model.serverTitle)
+ .navigationSubtitle("Public Chat")
.navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .news:
NewsView()
.navigationTitle(model.serverTitle)
+ .navigationSubtitle("Newsgroup")
.navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .board:
MessageBoardView()
.navigationTitle(model.serverTitle)
+ .navigationSubtitle("Message Board")
.navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .files:
FilesView()
.navigationTitle(model.serverTitle)
+ .navigationSubtitle("Shared Files")
.navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .user(let userID):
+ let user = model.users.first(where: { $0.id == userID })
MessageView(userID: userID)
.navigationTitle(model.serverTitle)
+ .navigationSubtitle(user?.name ?? "Private Message")
.navigationSplitViewColumnWidth(min: 250, ideal: 500)
+ .onAppear {
+ model.markInstantMessagesAsRead(userID: userID)
+ }
}
}
}