aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
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)