aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2024-05-22 11:40:53 -0700
committerDustin Mierau <dustin@mierau.me>2024-05-22 11:40:53 -0700
commitc7a2970c7f17a1b19a76d9cb6addda8fb877a63a (patch)
tree20f52495dd9e599d7b3942ba8357c79bc394822d /Hotline
parent39255f49250e3b0ccbd42661614a7c8a3e0d4fb7 (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.json23
-rw-r--r--Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post.pngbin0 -> 392 bytes
-rw-r--r--Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@2x.pngbin0 -> 701 bytes
-rw-r--r--Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@3x.pngbin0 -> 1002 bytes
-rw-r--r--Hotline/Models/Hotline.swift4
-rw-r--r--Hotline/Shared/TextView.swift119
-rw-r--r--Hotline/Utility/FoundationExtensions.swift22
-rw-r--r--Hotline/macOS/MessageBoardEditorView.swift117
-rw-r--r--Hotline/macOS/MessageBoardView.swift58
-rw-r--r--Hotline/macOS/NewsEditorView.swift18
-rw-r--r--Hotline/macOS/NewsView.swift2
-rw-r--r--Hotline/macOS/ServerView.swift9
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
new file mode 100644
index 0000000..80f0f73
--- /dev/null
+++ b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post.png
Binary files differ
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
new file mode 100644
index 0000000..990e5fd
--- /dev/null
+++ b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@2x.png
Binary files differ
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
new file mode 100644
index 0000000..6f293b5
--- /dev/null
+++ b/Hotline/Assets.xcassets/Message Board Post.imageset/Message Board Post@3x.png
Binary files differ
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)
}