diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-05-22 11:40:53 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-05-22 11:40:53 -0700 |
| commit | c7a2970c7f17a1b19a76d9cb6addda8fb877a63a (patch) | |
| tree | 20f52495dd9e599d7b3942ba8357c79bc394822d /Hotline | |
| parent | 39255f49250e3b0ccbd42661614a7c8a3e0d4fb7 (diff) | |
Redesign of Message Board post sheet. Tweaks to NewsEditor. Disable smart quotes in message board and news editor text view. Hide news section on older servers.
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Assets.xcassets/Message Board Post.imageset/Contents.json | 23 | ||||
| -rw-r--r-- | Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post.png | bin | 0 -> 392 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@2x.png | bin | 0 -> 701 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@3x.png | bin | 0 -> 1002 bytes | |||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 4 | ||||
| -rw-r--r-- | Hotline/Shared/TextView.swift | 119 | ||||
| -rw-r--r-- | Hotline/Utility/FoundationExtensions.swift | 22 | ||||
| -rw-r--r-- | Hotline/macOS/MessageBoardEditorView.swift | 117 | ||||
| -rw-r--r-- | Hotline/macOS/MessageBoardView.swift | 58 | ||||
| -rw-r--r-- | Hotline/macOS/NewsEditorView.swift | 18 | ||||
| -rw-r--r-- | Hotline/macOS/NewsView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 9 |
12 files changed, 332 insertions, 40 deletions
diff --git a/Hotline/Assets.xcassets/Message Board Post.imageset/Contents.json b/Hotline/Assets.xcassets/Message Board Post.imageset/Contents.json new file mode 100644 index 0000000..8147f45 --- /dev/null +++ b/Hotline/Assets.xcassets/Message Board Post.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Message Board Post.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Message Board Post@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Message Board Post@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post.png b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post.png Binary files differnew file mode 100644 index 0000000..80f0f73 --- /dev/null +++ b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post.png diff --git a/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@2x.png b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@2x.png Binary files differnew file mode 100644 index 0000000..990e5fd --- /dev/null +++ b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@2x.png diff --git a/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@3x.png b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@3x.png Binary files differnew file mode 100644 index 0000000..6f293b5 --- /dev/null +++ b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@3x.png diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index ce917cd..93f1762 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -259,7 +259,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate { @MainActor func postToMessageBoard(text: String) { self.client.sendPostMessageBoard(text: text) } - + @MainActor func getFileList(path: [String] = []) async -> [FileInfo] { return await withCheckedContinuation { [weak self] continuation in self?.client.sendGetFileList(path: path) { [weak self] files in @@ -422,6 +422,8 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate { } @MainActor func postNewsArticle(title: String, body: String, at path: [String], parentID: UInt32 = 0) async -> Bool { + + return await withCheckedContinuation { [weak self] continuation in guard let client = self?.client else { continuation.resume(returning: false) diff --git a/Hotline/Shared/TextView.swift b/Hotline/Shared/TextView.swift new file mode 100644 index 0000000..47746e2 --- /dev/null +++ b/Hotline/Shared/TextView.swift @@ -0,0 +1,119 @@ +import SwiftUI + +struct BetterTextEditor: NSViewRepresentable { + + @Environment(\.lineSpacing) private var lineSpacing + + @Binding private var text: String + + private var customizations: [(NSTextView) -> Void] = [] + + init(text: Binding<String>) { + self._text = text + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeNSView(context: Context) -> NSScrollView { + let scrollview = NSTextView.scrollablePlainDocumentContentTextView() + let textview = scrollview.documentView as! NSTextView + + textview.string = self.text + textview.delegate = context.coordinator + +// let p = NSMutableParagraphStyle() +// p.lineSpacing = self.lineSpacing +//// textview.defaultParagraphStyle = p +// textview.typingAttributes = [ +// .paragraphStyle: p +// ] + + textview.isEditable = true + textview.isRichText = false + textview.allowsUndo = true + textview.isFieldEditor = false + textview.usesAdaptiveColorMappingForDarkAppearance = true + textview.drawsBackground = false // true + textview.usesRuler = false + textview.usesFindBar = false + textview.isIncrementalSearchingEnabled = false + textview.isAutomaticQuoteSubstitutionEnabled = false + textview.isAutomaticDashSubstitutionEnabled = false + textview.isAutomaticSpellingCorrectionEnabled = true + textview.isAutomaticDataDetectionEnabled = false + textview.isAutomaticLinkDetectionEnabled = false + textview.usesInspectorBar = false + textview.usesFontPanel = false + textview.importsGraphics = false + textview.allowsImageEditing = false + textview.displaysLinkToolTips = true + textview.backgroundColor = NSColor.textBackgroundColor + textview.textContainerInset = NSSize(width: 16, height: 16) + textview.isContinuousSpellCheckingEnabled = true + textview.setSelectedRange(NSMakeRange(0, 0)) + self.customizations.forEach { $0(textview) } + + scrollview.scrollerStyle = .overlay + + return scrollview + } + + func updateNSView(_ nsView: NSScrollView, context: Context) { + let textview = nsView.documentView as! NSTextView + + if textview.string != text { + textview.string = text + } + + self.customizations.forEach { $0(textview) } + } + + func betterEditorFont(_ font: NSFont) -> Self { + self.customized { $0.font = font } + } + + func betterEditorParagraphStyle(_ paragraphStyle: NSParagraphStyle) -> Self { + self.customized { $0.defaultParagraphStyle = paragraphStyle } + } + + func betterEditorAutomaticDashSubstitution(_ enabled: Bool) -> Self { + self.customized { $0.isAutomaticDashSubstitutionEnabled = enabled } + } + + func betterEditorAutomaticQuoteSubstitution(_ enabled: Bool) -> Self { + self.customized { $0.isAutomaticQuoteSubstitutionEnabled = enabled } + } + + func betterEditorAutomaticSpellingCorrection(_ enabled: Bool) -> Self { + self.customized { $0.isAutomaticSpellingCorrectionEnabled = enabled } + } + + func betterEditorTextInset(_ size: NSSize) -> Self { + self.customized { $0.textContainerInset = size } + } + + class Coordinator: NSObject, NSTextViewDelegate { + var parent: BetterTextEditor + + init(_ parent: BetterTextEditor) { + self.parent = parent + } + + func textDidChange(_ notification: Notification) { + guard let textview = notification.object as? NSTextView else { + return + } + self.parent.text = textview.string + } + } +} + +private extension BetterTextEditor { + private func customized(_ customization: @escaping (NSTextView) -> Void) -> Self { + var copy = self + copy.customizations.append(customization) + return copy + } +} diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift index ebd179a..14e76d5 100644 --- a/Hotline/Utility/FoundationExtensions.swift +++ b/Hotline/Utility/FoundationExtensions.swift @@ -7,6 +7,28 @@ enum Endianness { } extension String { +// func convertToPlainText() -> String { +// var newString = self +// +// let map: [Character: Character] = [ +// "“": ", +// "”": ", +// "‘": "'", +// "’": "'", +// "\n": "\r" +// ] +// +// newString = newString.replacingOccurrences(of: "\u{0060}", with: "'") +// newString = newString.replacingOccurrences(of: "\u{00b4}", with: "'") +// newString = newString.replacingOccurrences(of: "\u{2018}", with: "'") +// newString = newString.replacingOccurrences(of: "\u{2019}", with: "'") +// newString = newString.replacingOccurrences(of: "\u{201c}", with: "'") +// newString = newString.replacingOccurrences(of: "\u{201d}", with: "'") +// newString = newString.replacingOccurrences(of: "\"", with: "\"") +// +// return newString +// } + func replyToString() -> String { if self.range(of: "^Re:", options: [.regularExpression, .caseInsensitive]) != nil { return String(self) diff --git a/Hotline/macOS/MessageBoardEditorView.swift b/Hotline/macOS/MessageBoardEditorView.swift new file mode 100644 index 0000000..1b49f4e --- /dev/null +++ b/Hotline/macOS/MessageBoardEditorView.swift @@ -0,0 +1,117 @@ +import SwiftUI + +private enum FocusFields { + case body +} + +struct MessageBoardEditorView: View { + @Environment(\.controlActiveState) private var controlActiveState + @Environment(\.colorScheme) private var colorScheme + @Environment(\.dismiss) private var dismiss + @Environment(Hotline.self) private var model: Hotline + + @State private var text: String = "" + @State private var sending: Bool = false + + @FocusState private var focusedField: FocusFields? + + func sendPost() async { + sending = true + + let cleanedText = text.trimmingCharacters(in: .whitespacesAndNewlines) + + await model.postToMessageBoard(text: cleanedText) + let _ = await model.getMessageBoard() + +// let success = await model.postNewsArticle(title: title, body: text, at: path, parentID: parentID) +// if success { +// await model.getNewsList(at: path) +// } + + sending = false + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .center, spacing: 0) { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + .opacity(0.5) + } + .buttonStyle(.plain) + .frame(width: 16, height: 16) + + Spacer() + + Image("Message Board Post") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .padding(.trailing, 6) + + Text("New Post") + .fontWeight(.semibold) + .lineLimit(1) + .truncationMode(.middle) + + Spacer() + + if sending { + ProgressView() + .controlSize(.small) + .frame(width: 22, height: 22) + } + else { + Button { + sending = true + model.postToMessageBoard(text: text) + Task { + let _ = await model.getMessageBoard() + Task { @MainActor in + sending = false + dismiss() + } + } + } label: { + Image(systemName: "arrow.up.circle.fill") + .resizable() + .renderingMode(.template) + .scaledToFit() + .foregroundColor(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .secondary : .accentColor) + } + .buttonStyle(.plain) + .frame(width: 22, height: 22) + .help("Post to Message Board") + .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + } + .frame(maxWidth: .infinity) + .padding() + + Divider() + + BetterTextEditor(text: $text) + .betterEditorFont(NSFont.systemFont(ofSize: 14.0)) + .betterEditorAutomaticSpellingCorrection(true) + .betterEditorTextInset(.init(width: 16, height: 18)) + .lineSpacing(20) + .background(Color(nsColor: .textBackgroundColor)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .focused($focusedField, equals: .body) + } + .frame(minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 300, idealHeight: 500, maxHeight: .infinity) + .background(Color(nsColor: .textBackgroundColor)) + .presentationCompactAdaptation(.sheet) + .onAppear { + focusedField = .body + } + .onDisappear { + dismiss() + } + } +} diff --git a/Hotline/macOS/MessageBoardView.swift b/Hotline/macOS/MessageBoardView.swift index 2bd3713..ba9968b 100644 --- a/Hotline/macOS/MessageBoardView.swift +++ b/Hotline/macOS/MessageBoardView.swift @@ -3,9 +3,8 @@ import SwiftUI struct MessageBoardView: View { @Environment(Hotline.self) private var model: Hotline - @State private var initialLoadComplete = false - @State private var composerDisplayed = false - @State private var composerText = "" + @State private var composerDisplayed: Bool = false + @State private var composerText: String = "" var body: some View { NavigationStack { @@ -54,31 +53,36 @@ struct MessageBoardView: View { } } .sheet(isPresented: $composerDisplayed) { - TextEditor(text: $composerText) - .padding() - .font(.system(size: 13)) - .lineSpacing(4) - .background(Color(nsColor: .textBackgroundColor)) + MessageBoardEditorView() + .frame(maxWidth: .infinity, maxHeight: .infinity) .frame(idealWidth: 450, idealHeight: 350) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - composerDisplayed.toggle() - } - } - - ToolbarItem(placement: .primaryAction) { - Button("Post") { - composerDisplayed.toggle() - let text = composerText - composerText = "" - model.postToMessageBoard(text: text) - Task { - await model.getMessageBoard() - } - } - } - } +// RichTextEditor(text: $composerText) +// .richEditorFont(NSFont.systemFont(ofSize: 16.0)) +// .richEditorAutomaticDashSubstitution(false) +// .richEditorAutomaticQuoteSubstitution(false) +// .richEditorAutomaticSpellingCorrection(false) +// .background(Color(nsColor: .textBackgroundColor)) +// .frame(maxWidth: .infinity, maxHeight: .infinity) +// .frame(idealWidth: 450, idealHeight: 350) +// .toolbar { +// ToolbarItem(placement: .cancellationAction) { +// Button("Cancel") { +// composerDisplayed.toggle() +// } +// } +// +// ToolbarItem(placement: .primaryAction) { +// Button("Post") { +// composerDisplayed.toggle() +// let text = composerText +// composerText = "" +// model.postToMessageBoard(text: text) +// Task { +// await model.getMessageBoard() +// } +// } +// } +// } } .toolbar { ToolbarItem(placement:.primaryAction) { diff --git a/Hotline/macOS/NewsEditorView.swift b/Hotline/macOS/NewsEditorView.swift index ff33281..a6f72ec 100644 --- a/Hotline/macOS/NewsEditorView.swift +++ b/Hotline/macOS/NewsEditorView.swift @@ -110,17 +110,12 @@ struct NewsEditorView: View { Divider() - TextEditor(text: $text) - .textEditorStyle(.plain) - .font(.system(size: 14, design: .monospaced)) - .lineSpacing(3) - .padding(16) - .contentMargins(.top, -16.0, for: .scrollIndicators) - .contentMargins(.bottom, -16.0, for: .scrollIndicators) - .contentMargins(.trailing, -16.0, for: .scrollIndicators) - .scrollClipDisabled() + BetterTextEditor(text: $text) + .betterEditorFont(NSFont.monospacedSystemFont(ofSize: 14.0, weight: .regular)) + .betterEditorAutomaticSpellingCorrection(true) + .betterEditorTextInset(.init(width: 16, height: 18)) + .background(Color(nsColor: .textBackgroundColor)) .frame(maxWidth: .infinity, maxHeight: .infinity) - .clipped() .focused($focusedField, equals: .body) HStack(alignment: .center) { @@ -147,6 +142,9 @@ struct NewsEditorView: View { if !title.isEmpty { focusedField = .body } + else { + focusedField = .title + } } .onDisappear { dismiss() diff --git a/Hotline/macOS/NewsView.swift b/Hotline/macOS/NewsView.swift index 8e10b19..4469585 100644 --- a/Hotline/macOS/NewsView.swift +++ b/Hotline/macOS/NewsView.swift @@ -58,7 +58,7 @@ struct NewsView: View { ) .fraction(splitFraction) .constraints(minPFraction: 0.1, minSFraction: 0.3) -// .hide(splitHidden) + .hide(splitHidden) .styling(color: colorScheme == .dark ? .black : Splitter.defaultColor, inset: 0, visibleThickness: 0.5, invisibleThickness: 5, hideSplitter: true) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(nsColor: .textBackgroundColor)) diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift index e3f0596..e342f73 100644 --- a/Hotline/macOS/ServerView.swift +++ b/Hotline/macOS/ServerView.swift @@ -151,6 +151,12 @@ struct ServerView: View { ServerMenuItem(type: .files, name: "Files", image: "folder"), ] + static var classicMenuItems: [ServerMenuItem] = [ + ServerMenuItem(type: .chat, name: "Chat", image: "bubble"), + ServerMenuItem(type: .board, name: "Board", image: "pin"), + ServerMenuItem(type: .files, name: "Files", image: "folder"), + ] + enum FocusFields { case address case login @@ -347,7 +353,8 @@ struct ServerView: View { var navigationList: some View { List(selection: $state.selection) { - ForEach(ServerView.menuItems) { menuItem in + // Don't show news on older servers. + ForEach(model.serverVersion < 151 ? ServerView.classicMenuItems : ServerView.menuItems) { menuItem in if menuItem.type == .chat { ListItemView(icon: menuItem.image, title: menuItem.name, unread: model.unreadPublicChat).tag(menuItem.type) } |