aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS/ChatView.swift
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-10-27 21:48:20 -0700
committerDustin Mierau <dustin@mierau.me>2025-10-27 21:48:20 -0700
commit4bb0ffba596e41a8309ba50d222248a0ba3eb42e (patch)
treefcd1c2b11c071a624d0b3b56ed16340272200c7d /Hotline/macOS/ChatView.swift
parent924012e6ee03fff5570e9362e29be9b04816b777 (diff)
Random project housekeeping/organizing source files.
Diffstat (limited to 'Hotline/macOS/ChatView.swift')
-rw-r--r--Hotline/macOS/ChatView.swift321
1 files changed, 0 insertions, 321 deletions
diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift
deleted file mode 100644
index 65087c6..0000000
--- a/Hotline/macOS/ChatView.swift
+++ /dev/null
@@ -1,321 +0,0 @@
-import SwiftUI
-
-enum FocusedField: Int, Hashable {
- case chatInput
-}
-
-struct ChatJoinedMessageView: View {
- let message: ChatMessage
-
- var body: some View {
- HStack(alignment: .center, spacing: 4) {
- 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: 4) {
- 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 {
- Text("\(username): ").fontWeight(.semibold) + Text(message.text.markdownToAttributedString())
- }
- else {
- Text(message.text)
- }
- Spacer()
- }
- .lineSpacing(4)
- .multilineTextAlignment(.leading)
- .textSelection(.enabled)
- .tint(Color("Link Color"))
- }
-}
-
-struct ChatView: View {
- @Environment(Hotline.self) private var model: Hotline
- @Environment(\.colorScheme) var colorScheme
- @Environment(\.dismiss) var dismiss
-
- @State private var scrollPos: Int?
- @State private var contentHeight: CGFloat = 0
-
- @State private var searchQuery: String = ""
- @State private var searchResults: [ChatMessage] = []
- @State private var isSearching: Bool = false
-
- @FocusState private var focusedField: FocusedField?
-
- @Namespace var bottomID
-
- private var bindableModel: Bindable<Hotline> {
- Bindable(model)
- }
-
-// @State private var showingExporter: Bool = false
-//
-// @State private var chatDocument: TextFile = TextFile()
-
- var displayedMessages: [ChatMessage] {
- searchQuery.isEmpty ? model.chat : searchResults
- }
-
- var body: some View {
- @Bindable var bindModel = model
-
- NavigationStack {
- ScrollViewReader { reader in
- VStack(alignment: .leading, spacing: 0) {
-
- // MARK: Scroll View
- ScrollView(.vertical) {
- LazyVStack(alignment: .leading, spacing: 8) {
-
- ForEach(displayedMessages) { msg in
- if msg.type == .agreement {
- VStack(alignment: .center, spacing: 16) {
- if let bannerImage = self.model.bannerImage {
- 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)
- }
- }
-
- ServerAgreementView(text: msg.text)
- }
- .padding(.vertical, 24)
- }
- else if msg.type == .server {
- ServerMessageView(message: msg.text)
- }
- 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 {
- ChatMessageView(message: msg)
- }
- }
- }
- .padding()
-
- VStack(spacing: 0) {}.id(bottomID)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
-// .defaultScrollAnchor(.bottom, for: .initialOffset)
- .defaultScrollAnchor(.bottom, for: .alignment)
- .defaultScrollAnchor(.bottom, for: .sizeChanges)
- .onChange(of: model.chat.count) {
- // Re-run search when new messages arrive to keep filter active
- if !searchQuery.isEmpty {
- performSearch()
- }
- reader.scrollTo(bottomID, anchor: .bottom)
- model.markPublicChatAsRead()
- }
- .onAppear {
- reader.scrollTo(bottomID, anchor: .bottom)
- self.focusedField = .chatInput
- }
- .onChange(of: self.model.bannerImage) {
- reader.scrollTo(bottomID, anchor: .bottom)
- }
- .onChange(of: searchQuery) {
- reader.scrollTo(bottomID, anchor: .bottom)
- }
- .onChange(of: isSearching) {
- reader.scrollTo(bottomID, anchor: .bottom)
- }
-
- // MARK: Input Divider
- Divider()
-
- // MARK: Input Bar
- HStack(alignment: .lastTextBaseline, spacing: 0) {
- TextField("", text: $bindModel.chatInput, axis: .vertical)
- .focused($focusedField, equals: .chatInput)
- .textFieldStyle(.plain)
- .lineLimit(1...5)
- .multilineTextAlignment(.leading)
- .onSubmit {
- if !model.chatInput.isEmpty {
- model.sendChat(model.chatInput, announce: NSEvent.modifierFlags.contains(.shift))
- }
- model.chatInput = ""
- }
- .frame(maxWidth: .infinity)
- .padding()
- }
- .frame(maxWidth: .infinity, minHeight: 28)
- .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
- .overlay(alignment: .leadingLastTextBaseline) {
- Image(systemName: "chevron.right").fontWeight(.semibold).opacity(0.4).offset(x: 16)
- }
- .onContinuousHover { phase in
- switch phase {
- case .active(_):
- NSCursor.iBeam.set()
- case .ended:
- NSCursor.arrow.set()
- break
- }
- }
- .onTapGesture(count: 1) {
- focusedField = .chatInput
- }
- }
- }
- .searchable(text: $searchQuery, isPresented: $isSearching, placement: .toolbar, prompt: "Search")
- .background(Button("", action: { isSearching = true }).keyboardShortcut("f").hidden())
- }
- .background(Color(nsColor: .textBackgroundColor))
-// .navigationTitle(model.serverTitle)
- .onChange(of: searchQuery) {
- performSearch()
- }
-// .toolbar {
-// ToolbarItem(placement: .primaryAction) {
-// Button {
-// 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):
-// print("Saved to \(url)")
-//
-// case .failure(let error):
-// print(error.localizedDescription)
-// }
-// self.chatDocument.text = ""
-// }
- }
-
- private func performSearch() {
- guard !searchQuery.isEmpty else {
- searchResults = []
- return
- }
-
- searchResults = model.searchChat(query: searchQuery)
- }
-
-// private func prepareChatDocument() -> Bool {
-// var text: String = String()
-//
-// self.chatDocument.text = ""
-// for msg in model.chat {
-// if msg.type == .agreement {
-// text.append(msg.text)
-// text.append("\n\n")
-// }
-// else if msg.type == .message {
-// if let username = msg.username {
-// text.append("\(username): \(msg.text)")
-// }
-// else {
-// text.append(msg.text)
-// }
-// text.append("\n")
-// }
-// else if msg.type == .status {
-// text.append(msg.text)
-// text.append("\n")
-// }
-// }
-//
-// if text.isEmpty {
-// return false
-// }
-//
-// self.chatDocument.text = text
-//
-// return true
-// }
-}
-
-#Preview {
- ChatView()
- .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient()))
-}