aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline/macOS')
-rw-r--r--Hotline/macOS/ChatView.swift288
-rw-r--r--Hotline/macOS/FilesView.swift3
-rw-r--r--Hotline/macOS/MessageBoardEditorView.swift2
-rw-r--r--Hotline/macOS/ServerAgreementView.swift27
-rw-r--r--Hotline/macOS/ServerMessageView.swift2
-rw-r--r--Hotline/macOS/ServerView.swift4
-rw-r--r--Hotline/macOS/SettingsView.swift19
7 files changed, 235 insertions, 110 deletions
diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift
index cd84369..12e6c94 100644
--- a/Hotline/macOS/ChatView.swift
+++ b/Hotline/macOS/ChatView.swift
@@ -4,6 +4,150 @@ enum FocusedField: Int, Hashable {
case chatInput
}
+struct ChatStatusMessageView: View {
+ let message: ChatMessage
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 8) {
+ Image(systemName: "arrow.right")
+ .resizable()
+ .scaledToFit()
+ .fontWeight(.semibold)
+ .foregroundStyle(.primary)
+ .frame(width: 14, height: 14)
+
+ Text(message.text)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .textSelection(.disabled)
+
+ Spacer()
+ }
+ .opacity(0.3)
+ }
+}
+
+
+struct ChatJoinedMessageView: View {
+ let message: ChatMessage
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 8) {
+ Image(systemName: "arrow.right")
+ .resizable()
+ .scaledToFit()
+ .fontWeight(.semibold)
+ .foregroundStyle(.primary)
+ .frame(width: 12, height: 12)
+
+ Text(message.text)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .fontWeight(.semibold)
+ .textSelection(.disabled)
+
+ Spacer()
+ }
+ .opacity(0.3)
+ }
+}
+
+struct ChatLeftMessageView: View {
+ let message: ChatMessage
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 8) {
+ Image(systemName: "arrow.left")
+ .resizable()
+ .scaledToFit()
+ .fontWeight(.semibold)
+ .foregroundStyle(.primary)
+ .frame(width: 12, height: 12)
+
+ Text(message.text)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .fontWeight(.semibold)
+ .textSelection(.disabled)
+
+ Spacer()
+ }
+ .opacity(0.3)
+ }
+}
+
+
+struct ChatDisconnectedMessageView: View {
+ let message: ChatMessage
+
+ var body: some View {
+ HStack {
+ Spacer()
+ }
+ .frame(height: 5)
+ .background(Color.primary)
+ .clipShape(.capsule)
+ .opacity(0.1)
+ }
+}
+
+struct ChatMessageView: View {
+ let message: ChatMessage
+
+ var body: some View {
+ HStack(alignment: .firstTextBaseline) {
+ if let username = message.username {
+ // if msg.text.isImageURL() {
+ // HStack(alignment: .bottom) {
+ // Text("**\(username):** ")
+ //
+ // let imageURL = URL(string: msg.text)!
+ // AsyncImage(url: imageURL) { phase in
+ // switch phase {
+ // case .failure:
+ // Text(LocalizedStringKey(msg.text.convertLinksToMarkdown()))
+ // .lineSpacing(4)
+ // .multilineTextAlignment(.leading)
+ // .textSelection(.enabled)
+ // .tint(Color("Link Color"))
+ // case .success(let img):
+ // Link(destination: imageURL) {
+ // img
+ // .resizable()
+ // .scaledToFit()
+ // .frame(maxWidth: 250, maxHeight: 150, alignment: .leading)
+ // .onAppear {
+ // reader.scrollTo(bottomID, anchor: .bottom)
+ // }
+ // }
+ // default:
+ // ProgressView().controlSize(.small)
+ // }
+ // }
+ //
+ // Spacer()
+ // }
+ // }
+ // else {
+ Text(LocalizedStringKey("**\(username):** \(message.text)".convertingLinksToMarkdown()))
+ .lineSpacing(4)
+ .multilineTextAlignment(.leading)
+ .textSelection(.enabled)
+ .tint(Color("Link Color"))
+ // }
+ }
+ else {
+ Text(message.text)
+ .lineSpacing(4)
+ .multilineTextAlignment(.leading)
+ .textSelection(.enabled)
+ .tint(Color("Link Color"))
+ }
+ Spacer()
+ }
+ }
+}
+
struct ChatView: View {
@Environment(Hotline.self) private var model: Hotline
@Environment(\.colorScheme) var colorScheme
@@ -20,7 +164,7 @@ struct ChatView: View {
@State private var showingExporter: Bool = false
@State private var chatDocument: TextFile = TextFile()
-
+
var body: some View {
NavigationStack {
ScrollViewReader { reader in
@@ -29,99 +173,64 @@ struct ChatView: View {
// MARK: Scroll View
GeometryReader { gm in
ScrollView(.vertical) {
- LazyVStack(alignment: .leading) {
+ LazyVStack(alignment: .leading, spacing: 8) {
ForEach(model.chat) { msg in
-
- // MARK: Agreement
if msg.type == .agreement {
-#if os(iOS)
+ VStack(alignment: .center, spacing: 16) {
if let bannerImage = self.model.bannerImage {
- Image(uiImage: bannerImage)
- .resizable()
- .scaledToFit()
- .frame(maxWidth: 468.0)
- .clipShape(RoundedRectangle(cornerRadius: 3))
+ HStack(spacing: 0) {
+ Spacer(minLength: 0)
+ ZStack {
+ Image(nsImage: bannerImage)
+ .resizable()
+ .scaledToFit()
+ .frame(maxWidth: 468.0)
+ .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
+ .offset(y: 1.5)
+ .blur(radius: 4)
+ .opacity(0.2)
+
+ Image(nsImage: bannerImage)
+ .resizable()
+ .scaledToFit()
+ .frame(maxWidth: 468.0)
+ .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
+ }
+
+ Spacer(minLength: 0)
+ }
}
-#endif
- ServerAgreementView(text: msg.text)
- .padding(.bottom, 16)
+
+ ServerAgreementView(text: msg.text)
+ }
+ .padding(.vertical, 24)
}
// MARK: Server Message
else if msg.type == .server {
ServerMessageView(message: msg.text)
- .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
}
// MARK: Status
else if msg.type == .status {
- HStack {
- Spacer()
- Text(msg.text)
- .lineLimit(1)
- .truncationMode(.middle)
- .textSelection(.disabled)
- .opacity(0.3)
- Spacer()
- }
- .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
+ ChatStatusMessageView(message: msg)
+ }
+ else if msg.type == .joined {
+ ChatJoinedMessageView(message: msg)
+ }
+ else if msg.type == .left {
+ ChatLeftMessageView(message: msg)
+ }
+ else if msg.type == .signOut {
+ ChatDisconnectedMessageView(message: msg)
+ .padding(.vertical, 24)
}
else {
- HStack(alignment: .firstTextBaseline) {
- if let username = msg.username {
-// if msg.text.isImageURL() {
-// HStack(alignment: .bottom) {
-// Text("**\(username):** ")
-//
-// let imageURL = URL(string: msg.text)!
-// AsyncImage(url: imageURL) { phase in
-// switch phase {
-// case .failure:
-// Text(LocalizedStringKey(msg.text.convertLinksToMarkdown()))
-// .lineSpacing(4)
-// .multilineTextAlignment(.leading)
-// .textSelection(.enabled)
-// .tint(Color("Link Color"))
-// case .success(let img):
-// Link(destination: imageURL) {
-// img
-// .resizable()
-// .scaledToFit()
-// .frame(maxWidth: 250, maxHeight: 150, alignment: .leading)
-// .onAppear {
-// reader.scrollTo(bottomID, anchor: .bottom)
-// }
-// }
-// default:
-// ProgressView().controlSize(.small)
-// }
-// }
-//
-// Spacer()
-// }
-// }
-// else {
- Text(LocalizedStringKey("**\(username):** \(msg.text)".convertingLinksToMarkdown()))
- .lineSpacing(4)
- .multilineTextAlignment(.leading)
- .textSelection(.enabled)
- .tint(Color("Link Color"))
-// }
- }
- else {
- Text(msg.text)
- .lineSpacing(4)
- .multilineTextAlignment(.leading)
- .textSelection(.enabled)
- .tint(Color("Link Color"))
- }
- Spacer()
- }
- .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
+ ChatMessageView(message: msg)
}
}
}
.padding()
-
+
VStack(spacing: 0) {}.id(bottomID)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -136,6 +245,9 @@ struct ChatView: View {
.onChange(of: gm.size) {
reader.scrollTo(bottomID, anchor: .bottom)
}
+ .onChange(of: self.model.bannerImage) {
+ reader.scrollTo(bottomID, anchor: .bottom)
+ }
}
// MARK: Input Divider
@@ -160,7 +272,7 @@ struct ChatView: View {
.frame(maxWidth: .infinity, minHeight: 28)
.padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
.overlay(alignment: .leadingLastTextBaseline) {
- Image(systemName: "chevron.right").opacity(0.4).offset(x: 16)
+ Image(systemName: "chevron.right").fontWeight(.semibold).opacity(0.4).offset(x: 16)
}
.onContinuousHover { phase in
switch phase {
@@ -178,17 +290,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/FilesView.swift b/Hotline/macOS/FilesView.swift
index 3447b28..00a4e1e 100644
--- a/Hotline/macOS/FilesView.swift
+++ b/Hotline/macOS/FilesView.swift
@@ -234,7 +234,7 @@ struct FilesView: View {
private var searchStatusMessage: String? {
switch model.fileSearchStatus {
- case .searching(let processed, let pending):
+ case .searching(let processed, _):
let scanned = processed == 1 ? "folder" : "folders"
return "Searched \(processed) \(scanned)..."
case .completed(let processed):
@@ -248,7 +248,6 @@ struct FilesView: View {
if model.fileSearchResults.isEmpty {
return nil
}
- let count = model.fileSearchResults.count
let folderWord = processed == 1 ? "folder" : "folders"
return "Search cancelled"
case .failed(let message):
diff --git a/Hotline/macOS/MessageBoardEditorView.swift b/Hotline/macOS/MessageBoardEditorView.swift
index 029616f..474384e 100644
--- a/Hotline/macOS/MessageBoardEditorView.swift
+++ b/Hotline/macOS/MessageBoardEditorView.swift
@@ -20,7 +20,7 @@ struct MessageBoardEditorView: View {
let cleanedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
- await model.postToMessageBoard(text: cleanedText)
+ model.postToMessageBoard(text: cleanedText)
let _ = await model.getMessageBoard()
// let success = await model.postNewsArticle(title: title, body: text, at: path, parentID: parentID)
diff --git a/Hotline/macOS/ServerAgreementView.swift b/Hotline/macOS/ServerAgreementView.swift
index a1f96d9..e72de7d 100644
--- a/Hotline/macOS/ServerAgreementView.swift
+++ b/Hotline/macOS/ServerAgreementView.swift
@@ -1,6 +1,6 @@
import SwiftUI
-fileprivate let MAX_AGREEMENT_HEIGHT: CGFloat = 280
+fileprivate let MAX_AGREEMENT_HEIGHT: CGFloat = 340
struct ServerAgreementView: View {
let text: String
@@ -42,7 +42,7 @@ struct ServerAgreementView: View {
#elseif os(macOS)
.background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow))
#endif
- .overlay(
+ .overlay(alignment: .bottomTrailing) {
ZStack(alignment: .bottomTrailing) {
Group {
if !expandable || expanded {
@@ -54,25 +54,22 @@ struct ServerAgreementView: View {
expanded = true
}
}, label: {
- Color.black
- .opacity(0.00001)
- .frame(width: 32, height: 32)
- .overlay(
- Image(systemName: "arrow.up.left.and.arrow.down.right")
- .resizable()
- .scaledToFit()
- .fontWeight(.semibold)
- .frame(width: 12, height: 12)
- .foregroundColor(.primary.opacity(0.8))
- , alignment: .center)
+ Image(systemName: "arrow.up.and.down.circle.fill")
+ .resizable()
+ .scaledToFit()
+ .fontWeight(.semibold)
+ .frame(width: 16, height: 16)
+ .foregroundColor(.primary.opacity(0.5))
})
.buttonStyle(.plain)
+ .buttonBorderShape(.circle)
.help("Expand Server Agreement")
+ .padding([.trailing, .bottom], 16)
}
}
}
- , alignment: .bottomTrailing)
- .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
+ .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
diff --git a/Hotline/macOS/ServerMessageView.swift b/Hotline/macOS/ServerMessageView.swift
index 8413a87..6c3c60e 100644
--- a/Hotline/macOS/ServerMessageView.swift
+++ b/Hotline/macOS/ServerMessageView.swift
@@ -24,7 +24,7 @@ struct ServerMessageView: View {
#elseif os(macOS)
.background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow))
#endif
- .clipShape(RoundedRectangle(cornerRadius: 8))
+ .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index 49413fe..77b6dc8 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -307,9 +307,7 @@ struct ServerView: View {
.keyboardShortcut(.cancelAction)
Button("Connect") {
- Task {
- await connectToServer()
- }
+ connectToServer()
}
.controlSize(.regular)
diff --git a/Hotline/macOS/SettingsView.swift b/Hotline/macOS/SettingsView.swift
index 81a2f8d..d301abd 100644
--- a/Hotline/macOS/SettingsView.swift
+++ b/Hotline/macOS/SettingsView.swift
@@ -3,6 +3,7 @@ import SwiftUI
struct GeneralSettingsView: View {
@State private var username: String = ""
@State private var usernameChanged: Bool = false
+ @State private var showClearHistoryConfirmation: Bool = false
let saveTimer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
@@ -24,9 +25,27 @@ struct GeneralSettingsView: View {
preferences.username = self.username
}
}
+
+ Divider()
+
+ Button(role: .destructive) {
+ showClearHistoryConfirmation = true
+ } label: {
+ Text("Clear Chat History…")
+ }
}
.padding()
.frame(width: 392)
+ .confirmationDialog("Clear chat history?", isPresented: $showClearHistoryConfirmation, titleVisibility: .visible) {
+ Button("Clear Chat History", role: .destructive) {
+ Task {
+ await ChatStore.shared.clearAll()
+ }
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("This removes all saved chat logs across servers. Active chats will repopulate only with new messages.")
+ }
.onAppear {
self.username = preferences.username
self.usernameChanged = false