diff --git a/Mactrix.xcodeproj/project.pbxproj b/Mactrix.xcodeproj/project.pbxproj index d354a95..81b2a8d 100644 --- a/Mactrix.xcodeproj/project.pbxproj +++ b/Mactrix.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 348E99882F40ABC3009F57A9 /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 348E99872F40ABC3009F57A9 /* AsyncAlgorithms */; }; 34913F6E2EC0F532003034CB /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 34913F6D2EC0F532003034CB /* MatrixRustSDK */; }; 34913F702EC0F59F003034CB /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 34913F6F2EC0F59F003034CB /* Models */; }; + 349B041C2F5D77E80023FBF4 /* MessageFormatting in Frameworks */ = {isa = PBXBuildFile; productRef = 349B041B2F5D77E80023FBF4 /* MessageFormatting */; }; 34F7225F2EB531F40007B2A4 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 34F7225E2EB531F40007B2A4 /* KeychainAccess */; }; /* End PBXBuildFile section */ @@ -46,6 +47,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 349B041C2F5D77E80023FBF4 /* MessageFormatting in Frameworks */, 3409F4EC2EBFD8D4009537B4 /* UI in Frameworks */, 34913F6E2EC0F532003034CB /* MatrixRustSDK in Frameworks */, 345E77E82ED9F309002E5B9A /* Utils in Frameworks */, @@ -109,6 +111,7 @@ 34913F6F2EC0F59F003034CB /* Models */, 345E77E72ED9F309002E5B9A /* Utils */, 348E99872F40ABC3009F57A9 /* AsyncAlgorithms */, + 349B041B2F5D77E80023FBF4 /* MessageFormatting */, ); productName = Mactrix; productReference = 343858422EB394590010922A /* Mactrix.app */; @@ -483,6 +486,10 @@ isa = XCSwiftPackageProductDependency; productName = Models; }; + 349B041B2F5D77E80023FBF4 /* MessageFormatting */ = { + isa = XCSwiftPackageProductDependency; + productName = MessageFormatting; + }; 34F7225E2EB531F40007B2A4 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = 34F7225D2EB531F40007B2A4 /* XCRemoteSwiftPackageReference "KeychainAccess" */; diff --git a/Mactrix.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mactrix.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c65f2ed..5f7bae5 100644 --- a/Mactrix.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mactrix.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d084e0c9b1d3b8ac948908f0ca98de1cca938f342f749686b8062637737a5cf4", + "originHash" : "cef1eb81361acd9acea13764bdc16f4787935d4b09f5dcc6bd8e53e93b503c29", "pins" : [ { "identity" : "keychainaccess", @@ -36,6 +36,24 @@ "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", "version" : "1.3.0" } + }, + { + "identity" : "zmarkupparser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ZhgChgLi/ZMarkupParser.git", + "state" : { + "revision" : "f25c9838e9e77ae49cc3f434ee26e669cb4043c7", + "version" : "1.12.0" + } + }, + { + "identity" : "znstextattachment", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ZhgChgLi/ZNSTextAttachment", + "state" : { + "revision" : "8c6c34e82eb31ad373b209cb88c2a2aff76c6cdf", + "version" : "1.1.9" + } } ], "version" : 3 diff --git a/Mactrix/Extensions/MatrixRustSDK+MessageContent.swift b/Mactrix/Extensions/MatrixRustSDK+MessageContent.swift index fc13dd8..078c727 100644 --- a/Mactrix/Extensions/MatrixRustSDK+MessageContent.swift +++ b/Mactrix/Extensions/MatrixRustSDK+MessageContent.swift @@ -1,13 +1,22 @@ import MatrixRustSDK protocol MessageContent { + var body: String { get } + var formatted: FormattedBody? { get } +} + +protocol MediaMessageContent { var filename: String { get } var caption: String? { get } var formattedCaption: FormattedBody? { get } var source: MediaSource { get } } -extension FileMessageContent: MessageContent {} -extension AudioMessageContent: MessageContent {} -extension VideoMessageContent: MessageContent {} -extension ImageMessageContent: MessageContent {} +extension FileMessageContent: MediaMessageContent {} +extension AudioMessageContent: MediaMessageContent {} +extension VideoMessageContent: MediaMessageContent {} +extension ImageMessageContent: MediaMessageContent {} + +extension EmoteMessageContent: MessageContent {} +extension NoticeMessageContent: MessageContent {} +extension TextMessageContent: MessageContent {} diff --git a/Mactrix/Views/ChatView/ChatMessageView.swift b/Mactrix/Views/ChatView/ChatMessageView.swift index 879b790..34549a8 100644 --- a/Mactrix/Views/ChatView/ChatMessageView.swift +++ b/Mactrix/Views/ChatView/ChatMessageView.swift @@ -82,9 +82,8 @@ struct ChatMessageView: View, UI.MessageEventActions { .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) case let .text(content: content): - Text(content.body.formatAsMarkdown) - .textSelection(.enabled) - .fixedSize(horizontal: false, vertical: true) + FormattedBodyView(messageContent: content) + //Text(content.body.formatAsMarkdown) case let .location(content: content): Text("Location: \(content.body) \(content.geoUri)").textSelection(.enabled) case let .other(msgtype: msgtype, body: body): diff --git a/Mactrix/Views/ChatView/FormattedBodyView.swift b/Mactrix/Views/ChatView/FormattedBodyView.swift new file mode 100644 index 0000000..d1c0f17 --- /dev/null +++ b/Mactrix/Views/ChatView/FormattedBodyView.swift @@ -0,0 +1,31 @@ +import MatrixRustSDK +import MessageFormatting +import SwiftUI + +struct FormattedBodyView: View { + @AppStorage("fontSize") private var fontSize = 13 + + let rawBody: String + let htmlBody: String? + + init(messageContent: some MessageContent) { + self.rawBody = messageContent.body + + if let formatted = messageContent.formatted, formatted.format == .html { + self.htmlBody = formatted.body + } else { + self.htmlBody = nil + } + } + + var body: some View { + if let htmlBody { + AttributedTextView(attributedString: parseFormattedBody(htmlBody, baseFontSize: CGFloat(fontSize))) + .fixedSize(horizontal: false, vertical: true) + } else { + Text(rawBody) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + } + } +} diff --git a/MactrixLibrary/Package.swift b/MactrixLibrary/Package.swift index fcd001c..6f570c4 100644 --- a/MactrixLibrary/Package.swift +++ b/MactrixLibrary/Package.swift @@ -17,10 +17,12 @@ let package = Package( targets: ["Models"] ), .library(name: "Utils", targets: ["Utils"]), + .library(name: "MessageFormatting", targets: ["MessageFormatting"]), + ], + dependencies: [ + .package(url: "https://github.com/ZhgChgLi/ZMarkupParser.git", from: "1.12.0"), + // .package(url: "https://github.com/matrix-org/matrix-rust-components-swift", from: "25.10.27"), ], - /* dependencies: [ - .package(url: "https://github.com/matrix-org/matrix-rust-components-swift", from: "25.10.27"), - ], */ targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. @@ -37,5 +39,11 @@ let package = Package( .target( name: "Models" ), + .target( + name: "MessageFormatting", + dependencies: [ + "ZMarkupParser", + ] + ), ] ) diff --git a/MactrixLibrary/Sources/MessageFormatting/AttributedTextView.swift b/MactrixLibrary/Sources/MessageFormatting/AttributedTextView.swift new file mode 100644 index 0000000..10d8c61 --- /dev/null +++ b/MactrixLibrary/Sources/MessageFormatting/AttributedTextView.swift @@ -0,0 +1,36 @@ +import SwiftUI + +public struct AttributedTextView: NSViewRepresentable { + public let attributedString: NSAttributedString + + public init(attributedString: NSAttributedString) { + self.attributedString = attributedString + } + + public func makeNSView(context: Context) -> NSTextField { + let textField = NSTextField(labelWithAttributedString: attributedString) + + textField.isEditable = false + textField.isSelectable = true + textField.allowsEditingTextAttributes = true + + textField.lineBreakStrategy = .standard + textField.lineBreakMode = .byWordWrapping + textField.usesSingleLineMode = false + + return textField + } + + public func updateNSView(_ textField: NSTextField, context: Context) { + if textField.attributedStringValue != attributedString { + textField.attributedStringValue = attributedString + } + } + + public func sizeThatFits(_ proposal: ProposedViewSize, nsView textField: NSTextField, context: Context) -> CGSize? { + guard let width = proposal.width, width > 0, width != .infinity else { return nil } + + textField.preferredMaxLayoutWidth = width + return textField.cell?.cellSize(forBounds: NSRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)) + } +} diff --git a/MactrixLibrary/Sources/MessageFormatting/Parser.swift b/MactrixLibrary/Sources/MessageFormatting/Parser.swift new file mode 100644 index 0000000..fb75a5b --- /dev/null +++ b/MactrixLibrary/Sources/MessageFormatting/Parser.swift @@ -0,0 +1,76 @@ +import AppKit +import Foundation +import ZMarkupParser + +@MainActor +public func parseFormattedBody(_ body: String, baseFontSize: CGFloat = 13) -> NSAttributedString { + let headingParagraphSpacing = MarkupStyleParagraphStyle( + // paragraphSpacing: 5, + paragraphSpacingBefore: baseFontSize * 0.8 + ) + + let parser = ZHTMLParserBuilder + .initWithDefault() + .set(rootStyle: MarkupStyle(font: MarkupStyleFont(size: baseFontSize))) + .add( + H1_HTMLTagName(), + withCustomStyle: MarkupStyle( + font: MarkupStyleFont(size: baseFontSize * 1.8), + paragraphStyle: headingParagraphSpacing + ) + ) + .add( + H2_HTMLTagName(), + withCustomStyle: MarkupStyle( + font: MarkupStyleFont(size: baseFontSize * 1.4), + paragraphStyle: headingParagraphSpacing + ) + ) + .add( + H3_HTMLTagName(), + withCustomStyle: MarkupStyle( + font: MarkupStyleFont(size: baseFontSize * 1.2), + paragraphStyle: headingParagraphSpacing + ) + ) + .add( + H4_HTMLTagName(), + withCustomStyle: MarkupStyle( + font: MarkupStyleFont(size: baseFontSize * 1.1, weight: .style(.medium)), + paragraphStyle: headingParagraphSpacing + ) + ) + .add( + H5_HTMLTagName(), + withCustomStyle: MarkupStyle( + font: MarkupStyleFont(size: baseFontSize, weight: .style(.semibold)), + paragraphStyle: headingParagraphSpacing + ) + ) + .add( + H6_HTMLTagName(), + withCustomStyle: MarkupStyle( + font: MarkupStyleFont(size: baseFontSize, weight: .style(.semibold)), + paragraphStyle: headingParagraphSpacing + ) + ) + .add( + P_HTMLTagName(), + withCustomStyle: MarkupStyle( + paragraphStyle: MarkupStyleParagraphStyle( + paragraphSpacing: baseFontSize * 0.4, + paragraphSpacingBefore: baseFontSize * 0.4 + ) + ) + ) + .add( + CODE_HTMLTagName(), + withCustomStyle: MarkupStyle( + font: MarkupStyleFont(size: baseFontSize, familyName: .familyNames(["Menlo"])) + // backgroundColor: .init(color: NSColor(red: 0.8, green: 0.8, blue: 1, alpha: 0.5)) + ) + ) + .build() + + return parser.render(body) +} diff --git a/MactrixLibrary/Sources/MessageFormatting/Preview.swift b/MactrixLibrary/Sources/MessageFormatting/Preview.swift new file mode 100644 index 0000000..38ea8ce --- /dev/null +++ b/MactrixLibrary/Sources/MessageFormatting/Preview.swift @@ -0,0 +1,72 @@ +import SwiftUI + +#Preview { + let sample = """ +
This is code
+ Another code line
+
+ this was all rendered from Element X
+ + +
+ This is bold, underline, strikethrough.
+
Here is a list of bullets.
+ +We can also make ordered lists.
+ ++ This is how a code block looks like. +
+ +
+ This is code
+ Another line
+
+
+
+ An example of inline code.
+