aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2024-05-08 20:53:10 -0700
committerDustin Mierau <dustin@mierau.me>2024-05-08 20:53:10 -0700
commit75b40db9adac93dde9df3ff76bb172c76e3d3a55 (patch)
tree1e9088849d7a32ac0f42c933e2b6ab50350ed196 /Hotline
parent01d32910bc1e75533c2f473011296fee6febbadb (diff)
Fix about box layout on open. Remove about box from window menu, add expand button to server agreement, add email address highlighting, style server messages, unread badge on admins are red now, play server message sound, some code cleanup.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Application-macOS.swift11
-rw-r--r--Hotline/Models/Hotline.swift7
-rw-r--r--Hotline/Utility/FoundationExtensions.swift12
-rw-r--r--Hotline/Utility/NSWindowBridge.swift25
-rw-r--r--Hotline/Utility/RegularExpressions.swift96
-rw-r--r--Hotline/macOS/AboutView.swift52
-rw-r--r--Hotline/macOS/ChatView.swift34
-rw-r--r--Hotline/macOS/ServerAgreementView.swift81
-rw-r--r--Hotline/macOS/ServerMessageView.swift26
-rw-r--r--Hotline/macOS/ServerView.swift8
-rw-r--r--Hotline/macOS/SettingsView.swift1
11 files changed, 298 insertions, 55 deletions
diff --git a/Hotline/Application-macOS.swift b/Hotline/Application-macOS.swift
index ccc4b15..09d6d91 100644
--- a/Hotline/Application-macOS.swift
+++ b/Hotline/Application-macOS.swift
@@ -49,9 +49,9 @@ struct Application: App {
.frame(minWidth: 250, minHeight: 250)
.environment(bookmarks)
}
- .keyboardShortcut(.init(.init("R"), modifiers: .command))
.defaultSize(width: 700, height: 550)
.defaultPosition(.center)
+ .keyboardShortcut(.init("R"), modifiers: .command)
.onChange(of: AppLaunchState.shared.launchState) {
if AppLaunchState.shared.launchState == .launched {
if Prefs.shared.showBannerToolbar {
@@ -60,21 +60,20 @@ struct Application: App {
}
}
+ // MARK: About Box
Window("About", id: "about") {
AboutView()
.ignoresSafeArea()
.background(Color.hotlineRed)
}
.windowResizability(.contentSize)
-// .windowStyle(.hiddenTitleBar)
- .windowStyle(HiddenTitleBarWindowStyle())
+ .windowStyle(.hiddenTitleBar)
.defaultPosition(.center)
+ .commandsRemoved() // Remove About that was automatically added to Window menu.
.commands {
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
- Button(action: {
+ Button("About Hotline") {
openWindow(id: "about")
- }) {
- Text("About Hotline")
}
}
}
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index d232533..598c787 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -723,7 +723,12 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
print("Hotline: received private message from \(user.name): \(message)")
if Prefs.shared.playPrivateMessageSound && Prefs.shared.playPrivateMessageSound {
- SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
+ if self.unreadInstantMessages[userID] == nil {
+ SoundEffectPlayer.shared.playSoundEffect(.serverMessage)
+ }
+ else {
+ SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
+ }
}
let instantMessage = InstantMessage(direction: .incoming, text: message.convertingLinksToMarkdown(), type: .message, date: Date())
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index 6270ac7..3a325be 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -11,12 +11,22 @@ extension String {
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self)
let matches = self.ranges(of: RegularExpressions.relaxedLink)
for match in matches {
- attributedString.addAttribute(.link, value: self[match], range: NSRange(match, in: self))
+ let matchString = String(self[match])
+ if matchString.isEmailAddress() {
+ attributedString.addAttribute(.link, value: "mailto:\(matchString)", range: NSRange(match, in: self))
+ }
+ else {
+ attributedString.addAttribute(.link, value: matchString, range: NSRange(match, in: self))
+ }
// attributedString.addAttribute(.underlineStyle, value: 1, range: NSRange(match, in: self))
}
return AttributedString(attributedString)
}
+ func isEmailAddress() -> Bool {
+ self.wholeMatch(of: RegularExpressions.emailAddress) != nil
+ }
+
func isWebURL() -> Bool {
guard let url = URL(string: self) else {
return false
diff --git a/Hotline/Utility/NSWindowBridge.swift b/Hotline/Utility/NSWindowBridge.swift
new file mode 100644
index 0000000..5cd7197
--- /dev/null
+++ b/Hotline/Utility/NSWindowBridge.swift
@@ -0,0 +1,25 @@
+import SwiftUI
+
+fileprivate class NSWindowAccessorView: NSView {
+ let executeBlock: (_ window: NSWindow? ) -> ()
+
+ init(_ inConfigFunction: @escaping (_ window: NSWindow? ) -> () ) {
+ executeBlock = inConfigFunction
+ super.init( frame: NSRect() )
+ }
+
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
+
+ public override func viewDidMoveToWindow() {
+ super.viewDidMoveToWindow()
+ executeBlock( self.window ) // We pass it through even if it is nil.
+ }
+}
+
+public struct NSWindowAccessor: NSViewRepresentable {
+ var configCode: (_ window: NSWindow? ) -> ()
+
+ public init(_ configCode: @escaping (_: NSWindow?) -> Void) { self.configCode = configCode }
+ public func makeNSView(context: Context) -> NSView { return NSWindowAccessorView( configCode ) }
+ public func updateNSView(_ nsView: NSView, context: Context) {}
+}
diff --git a/Hotline/Utility/RegularExpressions.swift b/Hotline/Utility/RegularExpressions.swift
index bb53f8c..c1f2a18 100644
--- a/Hotline/Utility/RegularExpressions.swift
+++ b/Hotline/Utility/RegularExpressions.swift
@@ -48,7 +48,7 @@ struct RegularExpressions {
// domain name
OneOrMore {
CharacterClass(
- .anyOf(".-"),
+ .anyOf(".-@"),
("a"..."z"),
("0"..."9")
)
@@ -138,4 +138,98 @@ struct RegularExpressions {
}
.anchorsMatchLineEndings()
.ignoresCase()
+
+ static let emailAddress = Regex {
+ ChoiceOf {
+ Anchor.startOfLine
+ Anchor.wordBoundary
+ }
+ Capture {
+ // username
+ OneOrMore {
+ CharacterClass(
+ .anyOf(".-_"),
+ ("a"..."z"),
+ ("0"..."9")
+ )
+ }
+ "@"
+ // domain name
+ OneOrMore {
+ CharacterClass(
+ .anyOf(".-"),
+ ("a"..."z"),
+ ("0"..."9")
+ )
+ }
+ // top-level domain name
+ "."
+ ChoiceOf {
+ "com"
+ "net"
+ "org"
+ "edu"
+ "gov"
+ "mil"
+ "aero"
+ "asia"
+ "biz"
+ "cat"
+ "coop"
+ "info"
+ "int"
+ "jobs"
+ "mobi"
+ "museum"
+ "name"
+ "pizza"
+ "post"
+ "pro"
+ "red"
+ "tel"
+ "today"
+ "travel"
+ "garden"
+ "online"
+ "ai"
+ "be"
+ "by"
+ "ca"
+ "co"
+ "de"
+ "er"
+ "es"
+ "fr"
+ "gs"
+ "ie"
+ "im"
+ "in"
+ "io"
+ "is"
+ "it"
+ "jp"
+ "la"
+ "ly"
+ "ma"
+ "md"
+ "me"
+ "my"
+ "nl"
+ "ps"
+ "pt"
+ "ja"
+ "st"
+ "to"
+ "tv"
+ "uk"
+ "ws"
+ }
+ }
+ ChoiceOf {
+ Anchor.endOfLine
+ Anchor.wordBoundary
+ }
+ }
+ .anchorsMatchLineEndings()
+ .ignoresCase()
}
diff --git a/Hotline/macOS/AboutView.swift b/Hotline/macOS/AboutView.swift
index 7bd1c54..1cf7dc2 100644
--- a/Hotline/macOS/AboutView.swift
+++ b/Hotline/macOS/AboutView.swift
@@ -77,11 +77,12 @@ struct AboutView: View {
}
}
.frame(height: 40)
-// .padding(.top, 4)
Spacer()
}
- .frame(width: 270)
+ .frame(width: 250)
+
+ Spacer()
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
@@ -118,17 +119,38 @@ struct AboutView: View {
Link(destination: contributor.webURL) {
HStack {
if let pictureURL = contributor.pictureURL {
- AsyncImage(url: pictureURL) { img in
- img
- .interpolation(.high)
- .resizable()
- .scaledToFit()
- .background(.white)
- } placeholder: {
- Color.white.opacity(0.2)
+ AsyncImage(url: pictureURL) { phase in
+ if let image = phase.image {
+ image
+ .interpolation(.high)
+ .resizable()
+ .scaledToFit()
+ .background(.white)
+ .frame(width: 32, height: 32)
+ } else if phase.error != nil {
+ Color.clear
+ .frame(width: 32, height: 32)
+ } else {
+ Color.white
+ .opacity(0.2)
+ .frame(width: 32, height: 32)
+ }
}
.frame(width: 32, height: 32)
.clipShape(Circle())
+
+// AsyncImage(url: pictureURL) { img in
+// img
+// .interpolation(.high)
+// .resizable()
+// .scaledToFit()
+// .background(.white)
+// } placeholder: {
+// Color.white.opacity(0.2)
+// .frame(width: 32, height: 32)
+// }
+// .frame(width: 32, height: 32)
+// .clipShape(Circle())
}
VStack(alignment: .leading, spacing: 2) {
@@ -143,16 +165,12 @@ struct AboutView: View {
.font(.system(size: 11))
.foregroundStyle(.white.opacity(0.4))
}
-
- Spacer()
}
}
}
}
}
- .ignoresSafeArea()
.scrollClipDisabled()
- .padding(.leading, 24)
}
.frame(width: 570, height: 330)
.background(
@@ -162,7 +180,7 @@ struct AboutView: View {
Spacer()
}
.frame(height: 330 + 100)
- .offset(x: 270)
+ .offset(x: 250)
}
)
.background(Color.hotlineRed)
@@ -191,7 +209,9 @@ struct AboutView: View {
}
}
- contributors = newContributors
+ withAnimation {
+ contributors = newContributors
+ }
}
func checkForUpdate() async {
diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift
index 98fb852..0875106 100644
--- a/Hotline/macOS/ChatView.swift
+++ b/Hotline/macOS/ChatView.swift
@@ -44,35 +44,17 @@ struct ChatView: View {
.clipShape(RoundedRectangle(cornerRadius: 3))
}
#endif
- ScrollView(.vertical) {
- HStack {
- Spacer()
- Text(msg.text.convertToAttributedStringWithLinks())
- .font(.system(size: 12))
- .fontDesign(.monospaced)
- .textSelection(.enabled)
- .tint(Color("Link Color"))
- .frame(maxWidth: 400, alignment: .center)
- .padding(16)
- Spacer()
- }
- }
- .frame(maxWidth: .infinity, maxHeight: 375)
- .scrollBounceBehavior(.basedOnSize)
-#if os(iOS)
- .background(Color("Agreement Background"))
-#elseif os(macOS)
- .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow))
-#endif
- .clipShape(RoundedRectangle(cornerRadius: 8))
- .padding(.bottom, 16)
+ ServerAgreementView(text: msg.text)
+ .padding(.bottom, 16)
}
// MARK: Server Message
else if msg.type == .server {
- Text(msg.text)
- .lineSpacing(4)
- .multilineTextAlignment(.leading)
- .textSelection(.enabled)
+ HStack {
+ Spacer()
+ ServerMessageView(message: msg.text)
+ Spacer()
+ }
+ .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
}
// MARK: Status
else if msg.type == .status {
diff --git a/Hotline/macOS/ServerAgreementView.swift b/Hotline/macOS/ServerAgreementView.swift
new file mode 100644
index 0000000..a1f96d9
--- /dev/null
+++ b/Hotline/macOS/ServerAgreementView.swift
@@ -0,0 +1,81 @@
+import SwiftUI
+
+fileprivate let MAX_AGREEMENT_HEIGHT: CGFloat = 280
+
+struct ServerAgreementView: View {
+ let text: String
+
+ @State private var expandable: Bool = false
+ @State private var expanded: Bool = false
+
+ var body: some View {
+ ScrollView(.vertical) {
+ HStack(alignment: .top) {
+ Spacer()
+ Text(text.convertToAttributedStringWithLinks())
+ .font(.system(size: 12))
+ .fontDesign(.monospaced)
+ .textSelection(.enabled)
+ .tint(Color("Link Color"))
+ .frame(maxWidth: 400)
+ .padding(16)
+ .background(
+ GeometryReader { geometry in
+ Color.clear.onAppear {
+ if geometry.size.height > MAX_AGREEMENT_HEIGHT {
+ expandable = true
+ }
+ else {
+ expandable = false
+ }
+ }
+ }
+ )
+ Spacer()
+ }
+ }
+ .scrollIndicators(.never)
+ .frame(maxWidth: .infinity, maxHeight: (expandable && expanded) ? nil : MAX_AGREEMENT_HEIGHT)
+ .scrollBounceBehavior(.basedOnSize)
+#if os(iOS)
+ .background(Color("Agreement Background"))
+#elseif os(macOS)
+ .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow))
+#endif
+ .overlay(
+ ZStack(alignment: .bottomTrailing) {
+ Group {
+ if !expandable || expanded {
+ EmptyView()
+ }
+ else {
+ Button(action: {
+ withAnimation(.easeOut(duration: 0.15)) {
+ 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)
+ })
+ .buttonStyle(.plain)
+ .help("Expand Server Agreement")
+ }
+ }
+ }
+ , alignment: .bottomTrailing)
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
+}
+
+#Preview {
+ ServerAgreementView(text: "Hello there and welcome to this server.")
+}
diff --git a/Hotline/macOS/ServerMessageView.swift b/Hotline/macOS/ServerMessageView.swift
new file mode 100644
index 0000000..373c2a6
--- /dev/null
+++ b/Hotline/macOS/ServerMessageView.swift
@@ -0,0 +1,26 @@
+import SwiftUI
+
+struct ServerMessageView: View {
+ let message: String
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 8) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .symbolRenderingMode(.multicolor)
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16)
+ Text("**\(message)**")
+ .lineSpacing(4)
+ .multilineTextAlignment(.leading)
+ .textSelection(.enabled)
+ }
+ .padding()
+ .background(Color("Agreement Background"))
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
+}
+
+#Preview {
+ ServerMessageView(message: "This server has something important to say.")
+}
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index 190bcc2..21d7cbb 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -400,19 +400,19 @@ struct ServerView: View {
}
Text(user.name)
- .foregroundStyle(user.isAdmin ? Color(hex: 0xE10000) : .primary)
+ .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary)
Spacer()
if model.hasUnreadInstantMessages(userID: user.id) {
Circle()
.frame(width: 6, height: 6)
+ .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary.opacity(0.5))
.padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 2))
- .opacity(0.5)
}
}
- .opacity(user.isIdle ? 0.6 : 1.0)
- .opacity(controlActiveState == .inactive ? 0.4 : 1.0)
+ .opacity(user.isIdle ? 0.5 : 1.0)
+ .opacity(controlActiveState == .inactive ? 0.5 : 1.0)
.tag(ServerNavigationType.user(userID: user.id))
}
}
diff --git a/Hotline/macOS/SettingsView.swift b/Hotline/macOS/SettingsView.swift
index 24cfddf..79f0156 100644
--- a/Hotline/macOS/SettingsView.swift
+++ b/Hotline/macOS/SettingsView.swift
@@ -71,6 +71,7 @@ struct IconSettingsView: View {
.interpolation(.none)
.scaledToFit()
.frame(width: 32, height: 16)
+ .help("Icon \(String(iconID))")
}
.tag(iconID)
.frame(width: 32, height: 32)