diff options
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Application-macOS.swift | 11 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 7 | ||||
| -rw-r--r-- | Hotline/Utility/FoundationExtensions.swift | 12 | ||||
| -rw-r--r-- | Hotline/Utility/NSWindowBridge.swift | 25 | ||||
| -rw-r--r-- | Hotline/Utility/RegularExpressions.swift | 96 | ||||
| -rw-r--r-- | Hotline/macOS/AboutView.swift | 52 | ||||
| -rw-r--r-- | Hotline/macOS/ChatView.swift | 34 | ||||
| -rw-r--r-- | Hotline/macOS/ServerAgreementView.swift | 81 | ||||
| -rw-r--r-- | Hotline/macOS/ServerMessageView.swift | 26 | ||||
| -rw-r--r-- | Hotline/macOS/ServerView.swift | 8 | ||||
| -rw-r--r-- | Hotline/macOS/SettingsView.swift | 1 |
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) |