diff options
| author | Dustin Mierau <dustin@mierau.me> | 2025-10-27 21:48:20 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2025-10-27 21:48:20 -0700 |
| commit | 4bb0ffba596e41a8309ba50d222248a0ba3eb42e (patch) | |
| tree | fcd1c2b11c071a624d0b3b56ed16340272200c7d /Hotline/macOS/ChatView.swift | |
| parent | 924012e6ee03fff5570e9362e29be9b04816b777 (diff) | |
Random project housekeeping/organizing source files.
Diffstat (limited to 'Hotline/macOS/ChatView.swift')
| -rw-r--r-- | Hotline/macOS/ChatView.swift | 321 |
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())) -} |