From 73208d70d21db7e526e371fa863acd0327caad9e Mon Sep 17 00:00:00 2001 From: Dustin Mierau Date: Thu, 2 May 2024 18:53:56 -0700 Subject: Add link highlighting to agreements and message board. Add basic markdown styling and link highlighting to chat messages. --- Hotline.xcodeproj/project.pbxproj | 14 +- .../Link Color.colorset/Contents.json | 10 +- Hotline/Shared/AnimatedImageView.swift | 98 ------------ Hotline/Shared/AsyncLinkPreview.swift | 57 +++++++ Hotline/Shared/FileImageView.swift | 98 ++++++++++++ Hotline/Utility/FoundationExtensions.swift | 43 ++++- Hotline/macOS/ChatView.swift | 173 ++++++++++++--------- Hotline/macOS/FilePreviewImageView.swift | 2 +- 8 files changed, 307 insertions(+), 188 deletions(-) delete mode 100644 Hotline/Shared/AnimatedImageView.swift create mode 100644 Hotline/Shared/AsyncLinkPreview.swift create mode 100644 Hotline/Shared/FileImageView.swift diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj index bafcc98..3bd7edb 100644 --- a/Hotline.xcodeproj/project.pbxproj +++ b/Hotline.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ DA4930BD2B4F8DD700822D0B /* NetSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4930BC2B4F8DD700822D0B /* NetSocket.swift */; }; DA4F2BF82B16A17200D8ADDC /* HotlineProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4F2BF72B16A17200D8ADDC /* HotlineProtocol.swift */; }; DA4F2C012B1A558E00D8ADDC /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4F2C002B1A558E00D8ADDC /* ChatView.swift */; platformFilter = ios; }; + DA55AC732BE42AF000034857 /* AsyncLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */; platformFilters = (macos, ); }; DA5753682B33E88A00FAC277 /* HotlineFileClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */; }; DA57536C2B36BA1D00FAC277 /* TextDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA57536B2B36BA1D00FAC277 /* TextDocument.swift */; }; DA6300972B24036B0034CBFD /* HotlineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6300962B24036B0034CBFD /* HotlineClient.swift */; }; @@ -32,7 +33,7 @@ DA77253F2B21176D006C5ABB /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA77253E2B21176D006C5ABB /* NewsView.swift */; platformFilter = ios; }; DA7725412B21435B006C5ABB /* ObservableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7725402B21435B006C5ABB /* ObservableScrollView.swift */; }; DA872B132BDDBF78008B1012 /* HotlinePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA872B122BDDBF78008B1012 /* HotlinePanel.swift */; platformFilters = (macos, ); }; - DA872B152BDDEE1A008B1012 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA872B142BDDEE1A008B1012 /* VisualEffectView.swift */; }; + DA872B152BDDEE1A008B1012 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA872B142BDDEE1A008B1012 /* VisualEffectView.swift */; platformFilters = (macos, ); }; DA9CAFBB2B126D5700CDA197 /* Application-macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBA2B126D5700CDA197 /* Application-macOS.swift */; platformFilters = (macos, ); }; DA9CAFBD2B126D5700CDA197 /* TrackerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBC2B126D5700CDA197 /* TrackerView.swift */; platformFilter = ios; }; DA9CAFC12B126D5800CDA197 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA9CAFC02B126D5800CDA197 /* Assets.xcassets */; }; @@ -41,7 +42,7 @@ DAAEE66B2B3FBC2100A5BA07 /* TransferInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAEE66A2B3FBC2100A5BA07 /* TransferInfo.swift */; }; DAAEE66D2B475F1400A5BA07 /* PreviewFileInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAEE66C2B475F1400A5BA07 /* PreviewFileInfo.swift */; }; DAAEE66F2B47625600A5BA07 /* FilePreviewImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAEE66E2B47625600A5BA07 /* FilePreviewImageView.swift */; platformFilters = (macos, ); }; - DAB4D87B2B4B78310048A05C /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D87A2B4B78310048A05C /* AnimatedImageView.swift */; platformFilters = (macos, ); }; + DAB4D87B2B4B78310048A05C /* FileImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D87A2B4B78310048A05C /* FileImageView.swift */; platformFilters = (macos, ); }; DAB4D87E2B4C8BCA0048A05C /* FilePreviewTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D87D2B4C8BCA0048A05C /* FilePreviewTextView.swift */; platformFilters = (macos, ); }; DAB4D8802B4C8E9A0048A05C /* URLAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D87F2B4C8E9A0048A05C /* URLAdditions.swift */; }; DAB4D8822B4C8FED0048A05C /* FileIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB4D8812B4C8FED0048A05C /* FileIconView.swift */; }; @@ -94,6 +95,7 @@ DA4930BC2B4F8DD700822D0B /* NetSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetSocket.swift; sourceTree = ""; }; DA4F2BF72B16A17200D8ADDC /* HotlineProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineProtocol.swift; sourceTree = ""; }; DA4F2C002B1A558E00D8ADDC /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; + DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncLinkPreview.swift; sourceTree = ""; }; DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFileClient.swift; sourceTree = ""; }; DA57536B2B36BA1D00FAC277 /* TextDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextDocument.swift; sourceTree = ""; }; DA6300962B24036B0034CBFD /* HotlineClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineClient.swift; sourceTree = ""; }; @@ -112,7 +114,7 @@ DAAEE66A2B3FBC2100A5BA07 /* TransferInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferInfo.swift; sourceTree = ""; }; DAAEE66C2B475F1400A5BA07 /* PreviewFileInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewFileInfo.swift; sourceTree = ""; }; DAAEE66E2B47625600A5BA07 /* FilePreviewImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewImageView.swift; sourceTree = ""; }; - DAB4D87A2B4B78310048A05C /* AnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = ""; }; + DAB4D87A2B4B78310048A05C /* FileImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileImageView.swift; sourceTree = ""; }; DAB4D87D2B4C8BCA0048A05C /* FilePreviewTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewTextView.swift; sourceTree = ""; }; DAB4D87F2B4C8E9A0048A05C /* URLAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLAdditions.swift; sourceTree = ""; }; DAB4D8812B4C8FED0048A05C /* FileIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileIconView.swift; sourceTree = ""; }; @@ -220,13 +222,14 @@ DAB4D87C2B4B783A0048A05C /* Shared */ = { isa = PBXGroup; children = ( - DAB4D87A2B4B78310048A05C /* AnimatedImageView.swift */, + DAB4D87A2B4B78310048A05C /* FileImageView.swift */, DAB4D8812B4C8FED0048A05C /* FileIconView.swift */, DAB4D8832B4CABEF0048A05C /* DataAdditions.swift */, DAB4D87F2B4C8E9A0048A05C /* URLAdditions.swift */, DA4930BC2B4F8DD700822D0B /* NetSocket.swift */, DA872B122BDDBF78008B1012 /* HotlinePanel.swift */, DA872B142BDDEE1A008B1012 /* VisualEffectView.swift */, + DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */, ); path = Shared; sourceTree = ""; @@ -426,7 +429,8 @@ DAE136BA2B9D1147007D8307 /* HotlinePanelView.swift in Sources */, 11A726082BE0672A000C1DA7 /* FileDetails.swift in Sources */, DA9CAFBD2B126D5700CDA197 /* TrackerView.swift in Sources */, - DAB4D87B2B4B78310048A05C /* AnimatedImageView.swift in Sources */, + DAB4D87B2B4B78310048A05C /* FileImageView.swift in Sources */, + DA55AC732BE42AF000034857 /* AsyncLinkPreview.swift in Sources */, DAE735032B30C0BB000C56F6 /* MessageView.swift in Sources */, DABE8C062B57A06100884D28 /* Bookmarks.swift in Sources */, DA32CD4B2B29318E0053B98B /* FileInfo.swift in Sources */, diff --git a/Hotline/Assets.xcassets/Link Color.colorset/Contents.json b/Hotline/Assets.xcassets/Link Color.colorset/Contents.json index 4373edf..e1d527b 100644 --- a/Hotline/Assets.xcassets/Link Color.colorset/Contents.json +++ b/Hotline/Assets.xcassets/Link Color.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xF1", - "green" : "0x61", - "red" : "0x39" + "blue" : "0xFF", + "green" : "0x86", + "red" : "0x1F" } }, "idiom" : "universal" @@ -24,8 +24,8 @@ "components" : { "alpha" : "1.000", "blue" : "0xFF", - "green" : "0xA3", - "red" : "0x8A" + "green" : "0xB0", + "red" : "0x6C" } }, "idiom" : "universal" diff --git a/Hotline/Shared/AnimatedImageView.swift b/Hotline/Shared/AnimatedImageView.swift deleted file mode 100644 index 0d92715..0000000 --- a/Hotline/Shared/AnimatedImageView.swift +++ /dev/null @@ -1,98 +0,0 @@ -import SwiftUI - -struct AnimatedImageView: NSViewRepresentable { - var image: NSImage? - - let minimumSize: CGSize = CGSize(width: 350, height: 350) - let presentationPaddingRatio: Double = 0.5 - - func makeNSView(context: Context) -> NSImageView { - let imageView = NSImageView() - imageView.imageScaling = .scaleProportionallyUpOrDown - imageView.animates = true - imageView.isEditable = false - imageView.allowsCutCopyPaste = true - imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - - if let img = self.image { - imageView.image = img - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.resizeWindowForImageView(imageView) - } - } - - return imageView - } - - func updateNSView(_ nsView: NSImageView, context: Context) { - nsView.image = self.image - } - - // MARK: - - - func resizeWindowForImageView(_ imageView: NSImageView) { - guard let window = imageView.window, let img = imageView.image else { - return - } - - var windowRect = window.contentLayoutRect - let windowChromeSize = CGSize(width: window.frame.width - windowRect.width, height: window.frame.height - windowRect.height) - - var windowMinSize: CGSize = windowRect.size - let centerPoint = CGPoint(x: window.frame.midX, y: window.frame.midY) - - windowRect.size = img.size - - if let screen = window.screen { - var paddedScreenSize = screen.frame.size - paddedScreenSize.width *= self.presentationPaddingRatio - paddedScreenSize.height *= self.presentationPaddingRatio - - windowMinSize = aspectFit(source: windowRect.size, bounds: self.minimumSize) - windowRect.size = aspectFit(source: windowRect.size, bounds: paddedScreenSize, minimum: windowMinSize) - } - - windowRect.size.width += windowChromeSize.width - windowRect.size.height += windowChromeSize.height - - windowRect.origin.x = centerPoint.x - windowRect.width / 2.0 - windowRect.origin.y = centerPoint.y - windowRect.height / 2.0 - - window.setFrame(windowRect, display: true, animate: true) - -// Do these APIs even work?? -// window.aspectRatio = windowRect.size -// window.contentAspectRatio = windowRect.size - } - - func aspectFit(source sourceSize: CGSize, bounds boundingSize: CGSize, minimum minSize: CGSize? = nil) -> CGSize { - let sourceAspectRatio = sourceSize.width / sourceSize.height - - var fitSize: CGSize = sourceSize - - if fitSize.width > boundingSize.width { - fitSize.width = boundingSize.width - fitSize.height = fitSize.width / sourceAspectRatio - } - - if fitSize.height > boundingSize.height { - fitSize.height = boundingSize.height - fitSize.width = fitSize.height * sourceAspectRatio - } - - if let m = minSize { - if fitSize.width < m.width { - fitSize.width = m.width - fitSize.height = fitSize.width / sourceAspectRatio - } - - if fitSize.height < m.height { - fitSize.height = m.height - fitSize.width = fitSize.height * sourceAspectRatio - } - } - - return fitSize - } -} diff --git a/Hotline/Shared/AsyncLinkPreview.swift b/Hotline/Shared/AsyncLinkPreview.swift new file mode 100644 index 0000000..9f0c4c2 --- /dev/null +++ b/Hotline/Shared/AsyncLinkPreview.swift @@ -0,0 +1,57 @@ +import SwiftUI +import LinkPresentation + +fileprivate class CustomLinkView: LPLinkView { + override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) } +} + +struct AsyncLinkPreview: View { + @State private var metadata: LPLinkMetadata? + @State private var isLoading = true + let url: URL? + + func fetchMetadata() async { + guard let url else { + self.isLoading = false + return + } + do { + let provider = LPMetadataProvider() + let metadata = try await provider.startFetchingMetadata(for: url) + self.metadata = metadata + self.isLoading = false + } catch { + self.isLoading = false + } + } + + var body: some View { + if isLoading { + ProgressView() + .controlSize(.small) + .task { + await self.fetchMetadata() + } + } else if let metadata = metadata { + LinkView(metadata: metadata) + .frame(width: 200) + } else { + Text(LocalizedStringKey(url!.absoluteString.convertLinksToMarkdown())) + .multilineTextAlignment(.leading) + .tint(Color("Link Color")) + } + } +} + +struct LinkView: NSViewRepresentable { + var metadata: LPLinkMetadata + + func makeNSView(context: Context) -> LPLinkView { + let linkView = CustomLinkView(metadata: metadata) + return linkView + } + + func updateNSView(_ nsView: LPLinkView, context: Context) { + // Nothing required + } +} diff --git a/Hotline/Shared/FileImageView.swift b/Hotline/Shared/FileImageView.swift new file mode 100644 index 0000000..0cc50f1 --- /dev/null +++ b/Hotline/Shared/FileImageView.swift @@ -0,0 +1,98 @@ +import SwiftUI + +struct FileImageView: NSViewRepresentable { + var image: NSImage? + + let minimumSize: CGSize = CGSize(width: 350, height: 350) + let presentationPaddingRatio: Double = 0.5 + + func makeNSView(context: Context) -> NSImageView { + let imageView = NSImageView() + imageView.imageScaling = .scaleProportionallyUpOrDown + imageView.animates = true + imageView.isEditable = false + imageView.allowsCutCopyPaste = true + imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + if let img = self.image { + imageView.image = img + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.resizeWindowForImageView(imageView) + } + } + + return imageView + } + + func updateNSView(_ nsView: NSImageView, context: Context) { + nsView.image = self.image + } + + // MARK: - + + func resizeWindowForImageView(_ imageView: NSImageView) { + guard let window = imageView.window, let img = imageView.image else { + return + } + + var windowRect = window.contentLayoutRect + let windowChromeSize = CGSize(width: window.frame.width - windowRect.width, height: window.frame.height - windowRect.height) + + var windowMinSize: CGSize = windowRect.size + let centerPoint = CGPoint(x: window.frame.midX, y: window.frame.midY) + + windowRect.size = img.size + + if let screen = window.screen { + var paddedScreenSize = screen.frame.size + paddedScreenSize.width *= self.presentationPaddingRatio + paddedScreenSize.height *= self.presentationPaddingRatio + + windowMinSize = aspectFit(source: windowRect.size, bounds: self.minimumSize) + windowRect.size = aspectFit(source: windowRect.size, bounds: paddedScreenSize, minimum: windowMinSize) + } + + windowRect.size.width += windowChromeSize.width + windowRect.size.height += windowChromeSize.height + + windowRect.origin.x = centerPoint.x - windowRect.width / 2.0 + windowRect.origin.y = centerPoint.y - windowRect.height / 2.0 + + window.setFrame(windowRect, display: true, animate: true) + +// Do these APIs even work?? +// window.aspectRatio = windowRect.size +// window.contentAspectRatio = windowRect.size + } + + func aspectFit(source sourceSize: CGSize, bounds boundingSize: CGSize, minimum minSize: CGSize? = nil) -> CGSize { + let sourceAspectRatio = sourceSize.width / sourceSize.height + + var fitSize: CGSize = sourceSize + + if fitSize.width > boundingSize.width { + fitSize.width = boundingSize.width + fitSize.height = fitSize.width / sourceAspectRatio + } + + if fitSize.height > boundingSize.height { + fitSize.height = boundingSize.height + fitSize.width = fitSize.height * sourceAspectRatio + } + + if let m = minSize { + if fitSize.width < m.width { + fitSize.width = m.width + fitSize.height = fitSize.width / sourceAspectRatio + } + + if fitSize.height < m.height { + fitSize.height = m.height + fitSize.width = fitSize.height * sourceAspectRatio + } + } + + return fitSize + } +} diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift index f2cd39c..9ad0d8b 100644 --- a/Hotline/Utility/FoundationExtensions.swift +++ b/Hotline/Utility/FoundationExtensions.swift @@ -6,17 +6,54 @@ enum Endianness { } extension String { + func convertToAttributedStringWithLinks(relaxed: Bool = false) -> AttributedString { + let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self) + let urlPattern = relaxed ? + #"(hotline|http|https)?(://)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"# : + #"(hotline|http|https)://[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"# + + guard let regex = try? NSRegularExpression(pattern: urlPattern, options: .caseInsensitive) else { + return AttributedString(attributedString) + } + + regex.enumerateMatches(in: self, range: NSMakeRange(0, self.count)) { (result: NSTextCheckingResult!, _, _) in + let range = result.range + if let newRange = Range(result.range, in: self) { + let str = String(self[newRange]) + attributedString.addAttribute(.link, value: str, range: result.range) + } + } + + return AttributedString(attributedString) + } + + func isWebURL() -> Bool { + guard let url = URL(string: self) else { + return false + } + switch url.scheme?.lowercased() { + case "http", "https": + return true + default: + return false + } + } + func isImageURL() -> Bool { guard let url = URL(string: self) else { return false } - let validImageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp"] - return validImageExtensions.contains(url.pathExtension.lowercased()) + switch url.pathExtension.lowercased() { + case "jpg", "jpeg", "png", "gif": + return true + default: + return false + } } func convertLinksToMarkdown() -> String { - let urlPattern = #"https?://[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"# + let urlPattern = #"(hotline|http|https)?(://)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"# guard let regex = try? NSRegularExpression(pattern: urlPattern, options: .caseInsensitive) else { return self diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift index 57814e2..35ff28e 100644 --- a/Hotline/macOS/ChatView.swift +++ b/Hotline/macOS/ChatView.swift @@ -24,20 +24,19 @@ struct ChatView: View { var body: some View { NavigationStack { ScrollViewReader { reader in - + VStack(alignment: .leading, spacing: 0) { // MARK: Scroll View GeometryReader { gm in ScrollView(.vertical) { LazyVStack(alignment: .leading) { - + ForEach(model.chat) { msg in // MARK: Agreement if msg.type == .agreement { - VStack(alignment: .center, spacing: 16) { - #if os(iOS) +#if os(iOS) if let bannerImage = self.model.bannerImage { Image(uiImage: bannerImage) .resizable() @@ -45,35 +44,29 @@ struct ChatView: View { .frame(maxWidth: 468.0) .clipShape(RoundedRectangle(cornerRadius: 3)) } - #endif - HStack(spacing: 0) { - Spacer() - - ScrollView(.vertical) { - HStack { - Text(LocalizedStringKey(msg.text.convertLinksToMarkdown())) - .font(.system(size: 12)) - .fontDesign(.monospaced) - .textSelection(.enabled) - .tint(Color("Link Color")) - Spacer() - } - .padding(16) +#endif + ScrollView(.vertical) { + HStack { + Spacer() + Text(msg.text.convertToAttributedStringWithLinks(relaxed: false)) + .font(.system(size: 12)) + .fontDesign(.monospaced) + .textSelection(.enabled) + .tint(Color("Link Color")) + .frame(maxWidth: 400, alignment: .center) + .padding(16) + Spacer() } - .scrollBounceBehavior(.basedOnSize) - .frame(maxWidth: 468, maxHeight: 375) - - Spacer() } - #if os(iOS) + .frame(maxWidth: .infinity, maxHeight: 375) + .scrollBounceBehavior(.basedOnSize) +#if os(iOS) .background(Color("Agreement Background")) - #elseif os(macOS) +#elseif os(macOS) .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow)) - #endif +#endif .clipShape(RoundedRectangle(cornerRadius: 8)) - } - .frame(maxWidth: .infinity) - .padding() + .padding(.bottom, 16) } // MARK: Status else if msg.type == .status { @@ -86,16 +79,49 @@ struct ChatView: View { .opacity(0.3) Spacer() } - .padding() + .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) } else { HStack(alignment: .firstTextBaseline) { if let username = msg.username { - Text(LocalizedStringKey("**\(username):** \(msg.text)".convertLinksToMarkdown())) - .lineSpacing(4) - .multilineTextAlignment(.leading) - .textSelection(.enabled) - .tint(Color("Link Color")) +// if msg.text.isImageURL() { +// HStack(alignment: .bottom) { +// Text("**\(username):** ") +// +// let imageURL = URL(string: msg.text)! +// AsyncImage(url: imageURL) { phase in +// switch phase { +// case .failure: +// Text(LocalizedStringKey(msg.text.convertLinksToMarkdown())) +// .lineSpacing(4) +// .multilineTextAlignment(.leading) +// .textSelection(.enabled) +// .tint(Color("Link Color")) +// case .success(let img): +// Link(destination: imageURL) { +// img +// .resizable() +// .scaledToFit() +// .frame(maxWidth: 250, maxHeight: 150, alignment: .leading) +// .onAppear { +// reader.scrollTo(bottomID, anchor: .bottom) +// } +// } +// default: +// ProgressView().controlSize(.small) +// } +// } +// +// Spacer() +// } +// } +// else { + Text(LocalizedStringKey("**\(username):** \(msg.text)".convertLinksToMarkdown())) + .lineSpacing(4) + .multilineTextAlignment(.leading) + .textSelection(.enabled) + .tint(Color("Link Color")) +// } } else { Text(LocalizedStringKey(msg.text.convertLinksToMarkdown())) @@ -106,68 +132,63 @@ struct ChatView: View { } Spacer() } - .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16)) + .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) } } EmptyView().id(bottomID) } - // .padding(.bottom, 12) + .padding() } - // .padding(.bottom, 60) .frame(maxWidth: .infinity, maxHeight: .infinity) .defaultScrollAnchor(.bottom) .onChange(of: model.chat.count) { -// withAnimation { reader.scrollTo(bottomID, anchor: .bottom) -// } } .onAppear { reader.scrollTo(bottomID, anchor: .bottom) -// focusedField = .chatInput } .onChange(of: gm.size) { reader.scrollTo(bottomID, anchor: .bottom) } } - } - - // MARK: Input Divider - Divider() - .padding([.top, .bottom], 0) - - // MARK: Input Bar - HStack(alignment: .lastTextBaseline, spacing: 0) { - TextField("", text: $input, axis: .vertical) - .focused($focusedField, equals: .chatInput) - .textFieldStyle(.plain) - .lineLimit(1...5) - .multilineTextAlignment(.leading) -// .frame(maxWidth: .infinity) - .onSubmit { - if !self.input.isEmpty { - model.sendChat(self.input) + + // MARK: Input Divider + Divider() + + // MARK: Input Bar + HStack(alignment: .lastTextBaseline, spacing: 0) { + TextField("", text: $input, axis: .vertical) + .focused($focusedField, equals: .chatInput) + .textFieldStyle(.plain) + .lineLimit(1...5) + .multilineTextAlignment(.leading) + // .frame(maxWidth: .infinity) + .onSubmit { + if !self.input.isEmpty { + model.sendChat(self.input) + } + self.input = "" } - self.input = "" + .frame(maxWidth: .infinity) + .padding() + } + .frame(maxWidth: .infinity, minHeight: 28) + .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16)) + .overlay(alignment: .leadingLastTextBaseline) { + Image(systemName: "chevron.right").opacity(0.4).offset(x: 16) + } + .onContinuousHover { phase in + switch phase { + case .active(_): + NSCursor.iBeam.set() + case .ended: + NSCursor.arrow.set() + break } - .frame(maxWidth: .infinity) - .padding() - } - .frame(maxWidth: .infinity, minHeight: 28) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 8, trailing: 16)) - .overlay(alignment: .leadingLastTextBaseline) { - Image(systemName: "chevron.right").opacity(0.4).offset(x: 16) - } - .onContinuousHover { phase in - switch phase { - case .active(_): - NSCursor.iBeam.set() - case .ended: - NSCursor.arrow.set() - break } - } - .onTapGesture { - focusedField = .chatInput + .onTapGesture { + focusedField = .chatInput + } } } } diff --git a/Hotline/macOS/FilePreviewImageView.swift b/Hotline/macOS/FilePreviewImageView.swift index 461f5a4..ae9a825 100644 --- a/Hotline/macOS/FilePreviewImageView.swift +++ b/Hotline/macOS/FilePreviewImageView.swift @@ -31,7 +31,7 @@ struct FilePreviewImageView: View { } else { if let image = preview?.image { - AnimatedImageView(image: image) + FileImageView(image: image) .frame(minWidth: 200, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity) } else { -- cgit