diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 6355bb174..85858db12 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -435,6 +435,7 @@ D63974A88CF2BC721F109C77 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; }; D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; }; D79F0F852C6A4255D5E616D2 /* UserNotificationControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */; }; + D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; }; D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; }; D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; @@ -734,6 +735,7 @@ 51DF91C374901E94D93276F1 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-MX"; path = "es-MX.lproj/Localizable.stringsdict"; sourceTree = ""; }; 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreen.swift; sourceTree = ""; }; 529513218340CC8419273165 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedString.swift; sourceTree = ""; }; 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = ""; }; 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewAdapter.swift; sourceTree = ""; }; 534A5C8FCDE2CBC50266B9F2 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = gl; path = gl.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1460,6 +1462,7 @@ 44BBB96FAA2F0D53C507396B /* Extensions */ = { isa = PBXGroup; children = ( + 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */, B6E89E530A8E92EC44301CA1 /* Bundle.swift */, A9FAFE1C2149E6AC8156ED2B /* Collection.swift */, 2141693488CE5446BB391964 /* Date.swift */, @@ -3037,6 +3040,7 @@ 9462C62798F47E39DCC182D2 /* Application.swift in Sources */, 74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */, 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */, + D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */, 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */, A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */, EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */, diff --git a/ElementX/Sources/Other/Extensions/AttributedString.swift b/ElementX/Sources/Other/Extensions/AttributedString.swift new file mode 100644 index 000000000..eaf12d555 --- /dev/null +++ b/ElementX/Sources/Other/Extensions/AttributedString.swift @@ -0,0 +1,37 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension AttributedString { + var formattedComponents: [AttributedStringBuilderComponent] { + runs[\.blockquote].map { value, range in + var attributedString = AttributedString(self[range]) + + // Remove trailing new lines if any + if attributedString.characters.last?.isNewline ?? false, + let range = attributedString.range(of: "\n", options: .backwards, locale: nil) { + attributedString.removeSubrange(range) + } + + let isBlockquote = value != nil + /// This is a temporary workaround until replies are retrieved from the SDK. + let isReply = isBlockquote && attributedString.characters.starts(with: "In reply to @") + + return AttributedStringBuilderComponent(attributedString: attributedString, isBlockquote: isBlockquote, isReply: isReply) + } + } +} diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index 54cfc3614..e03dedd87 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -72,6 +72,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) removeDefaultForegroundColor(mutableAttributedString) addLinks(mutableAttributedString) + detectPermalinks(mutableAttributedString) removeLinkColors(mutableAttributedString) replaceMarkedBlockquotes(mutableAttributedString) replaceMarkedCodeBlocks(mutableAttributedString) @@ -80,28 +81,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return try? AttributedString(mutableAttributedString, including: \.elementX) } - func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]? { - guard let attributedString else { - return nil - } - - return attributedString.runs[\.blockquote].map { value, range in - var attributedString = AttributedString(attributedString[range]) - - // Remove trailing new lines if any - if attributedString.characters.last?.isNewline ?? false, - let range = attributedString.range(of: "\n", options: .backwards, locale: nil) { - attributedString.removeSubrange(range) - } - - let isBlockquote = value != nil - /// This is a temporary workaround until replies are retrieved from the SDK. - let isReply = isBlockquote && attributedString.characters.starts(with: "In reply to @") - - return AttributedStringBuilderComponent(attributedString: attributedString, isBlockquote: isBlockquote, isReply: isReply) - } - } - // MARK: - Private private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) { @@ -115,7 +94,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return } - attributedString.addAttribute(.MXBlockquote, value: true, range: range) + attributedString.addAttribute(.MatrixBlockquote, value: true, range: range) } attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in @@ -125,7 +104,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } attributedString.removeAttribute(.backgroundColor, range: range) - attributedString.addAttribute(.MXBlockquote, value: true, range: range) + attributedString.addAttribute(.MatrixBlockquote, value: true, range: range) } } @@ -188,6 +167,27 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } } + private func detectPermalinks(_ attributedString: NSMutableAttributedString) { + attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + if value != nil { + if let url = value as? URL { + switch PermalinkBuilder.detectPermalink(in: url) { + case .userIdentifier(let identifier): + attributedString.addAttributes([.MatrixUserID: identifier], range: range) + case .roomIdentifier(let identifier): + attributedString.addAttributes([.MatrixRoomID: identifier], range: range) + case .roomAlias(let alias): + attributedString.addAttributes([.MatrixRoomAlias: alias], range: range) + case .event(let roomIdentifier, let eventIdentifier): + attributedString.addAttributes([.MatrixEventID: EventIDAttributeValue(roomID: roomIdentifier, eventID: eventIdentifier)], range: range) + case .none: + break + } + } + } + } + } + private func removeDefaultForegroundColor(_ attributedString: NSMutableAttributedString) { attributedString.enumerateAttribute(.foregroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in if value as? UIColor == UIColor.black { @@ -241,5 +241,9 @@ extension UIColor { extension NSAttributedString.Key { static let DTTextBlocks: NSAttributedString.Key = .init(rawValue: DTTextBlocksAttribute) - static let MXBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name) + static let MatrixBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name) + static let MatrixUserID: NSAttributedString.Key = .init(rawValue: UserIDAttribute.name) + static let MatrixRoomID: NSAttributedString.Key = .init(rawValue: RoomIDAttribute.name) + static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name) + static let MatrixEventID: NSAttributedString.Key = .init(rawValue: EventIDAttribute.name) } diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift index 943db54ca..0152bf187 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift @@ -26,6 +26,4 @@ protocol AttributedStringBuilderProtocol { func fromPlain(_ string: String?) -> AttributedString? func fromHTML(_ htmlString: String?) -> AttributedString? - - func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]? } diff --git a/ElementX/Sources/Other/HTMLParsing/ElementXAttributeScope.swift b/ElementX/Sources/Other/HTMLParsing/ElementXAttributeScope.swift index 777dad096..a14a92701 100644 --- a/ElementX/Sources/Other/HTMLParsing/ElementXAttributeScope.swift +++ b/ElementX/Sources/Other/HTMLParsing/ElementXAttributeScope.swift @@ -21,10 +21,40 @@ enum BlockquoteAttribute: AttributedStringKey { public static var name = "MXBlockquoteAttribute" } +enum UserIDAttribute: AttributedStringKey { + typealias Value = String + public static var name = "MXUserIDAttribute" +} + +enum RoomIDAttribute: AttributedStringKey { + typealias Value = String + public static var name = "MXRoomIDAttribute" +} + +enum RoomAliasAttribute: AttributedStringKey { + typealias Value = String + public static var name = "MXRoomAliasAttribute" +} + +struct EventIDAttributeValue: Hashable { + let roomID: String + let eventID: String +} + +enum EventIDAttribute: AttributedStringKey { + typealias Value = EventIDAttributeValue + public static var name = "MXEventIDAttribute" +} + extension AttributeScopes { struct ElementXAttributes: AttributeScope { let blockquote: BlockquoteAttribute + let userID: UserIDAttribute + let roomID: RoomIDAttribute + let roomAlias: RoomAliasAttribute + let eventID: EventIDAttribute + let swiftUI: SwiftUIAttributes let uiKit: UIKitAttributes } diff --git a/ElementX/Sources/Other/PermalinkBuilder.swift b/ElementX/Sources/Other/PermalinkBuilder.swift index a8a1ce606..5a54a77fc 100644 --- a/ElementX/Sources/Other/PermalinkBuilder.swift +++ b/ElementX/Sources/Other/PermalinkBuilder.swift @@ -25,13 +25,59 @@ enum PermalinkBuilderError: Error { case failedAddingPercentEncoding } +enum PermalinkType: Equatable { + case userIdentifier(String) + case roomIdentifier(String) + case roomAlias(String) + case event(roomIdentifier: String, eventIdentifier: String) +} + enum PermalinkBuilder { - static var uriComponentCharacterSet: CharacterSet = { + private static var uriComponentCharacterSet: CharacterSet = { var charset = CharacterSet.alphanumerics charset.insert(charactersIn: "-_.!~*'()") return charset }() + static func detectPermalink(in url: URL) -> PermalinkType? { + guard url.absoluteString.hasPrefix(ServiceLocator.shared.settings.permalinkBaseURL.absoluteString) else { + return nil + } + + guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + return nil + } + + guard var fragment = urlComponents.fragment else { + return nil + } + + if fragment.hasPrefix("/") { + fragment = String(fragment.dropFirst(1)) + } + + if let userIdentifierRange = MatrixEntityRegex.userIdentifierRegex.firstMatch(in: fragment, range: .init(location: 0, length: fragment.count))?.range { + return .userIdentifier((fragment as NSString).substring(with: userIdentifierRange)) + } + + if let roomAliasRange = MatrixEntityRegex.roomAliasRegex.firstMatch(in: fragment, range: .init(location: 0, length: fragment.count))?.range { + return .roomAlias((fragment as NSString).substring(with: roomAliasRange)) + } + + if let roomIdentifierRange = MatrixEntityRegex.roomIdentifierRegex.firstMatch(in: fragment, range: .init(location: 0, length: fragment.count))?.range { + let roomIdentifier = (fragment as NSString).substring(with: roomIdentifierRange) + + if let eventIdentifierRange = MatrixEntityRegex.eventIdentifierRegex.firstMatch(in: fragment, range: .init(location: 0, length: fragment.count))?.range { + let eventIdentifier = (fragment as NSString).substring(with: eventIdentifierRange) + return .event(roomIdentifier: roomIdentifier, eventIdentifier: eventIdentifier) + } + + return .roomIdentifier(roomIdentifier) + } + + return nil + } + static func permalinkTo(userIdentifier: String) throws -> URL { guard MatrixEntityRegex.isMatrixUserIdentifier(userIdentifier) else { throw PermalinkBuilderError.invalidUserIdentifier diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index e26534de4..ec18789c8 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -245,14 +245,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol case .react: callback?(.displayEmojiPicker(itemId: item.id)) case .copy: - UIPasteboard.general.string = item.text + UIPasteboard.general.string = item.body case .edit: state.bindings.composerFocused = true - state.bindings.composerText = item.text + state.bindings.composerText = item.body state.composerMode = .edit(originalItemId: item.id) case .quote: state.bindings.composerFocused = true - state.bindings.composerText = "> \(item.text)" + state.bindings.composerText = "> \(item.body)" case .copyPermalink: do { let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomID) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift index 1fd4b18fe..9b555310e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift @@ -22,10 +22,10 @@ struct EmoteRoomTimelineView: View { var body: some View { TimelineStyler(timelineItem: timelineItem) { - if let attributedComponents = timelineItem.attributedComponents { - FormattedBodyText(attributedComponents: attributedComponents) + if let attributedString = timelineItem.formattedBody { + FormattedBodyText(attributedString: attributedString) } else { - FormattedBodyText(text: timelineItem.text) + FormattedBodyText(text: timelineItem.body) } } } @@ -53,7 +53,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider { private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem { EmoteRoomTimelineItem(id: UUID().uuidString, - text: text, + body: text, timestamp: timestamp, groupState: .single, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift index a75011c96..5d9599b9d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift @@ -21,7 +21,7 @@ struct EncryptedRoomTimelineView: View { var body: some View { TimelineStyler(timelineItem: timelineItem) { - Label(timelineItem.text, systemImage: "lock.shield") + Label(timelineItem.body, systemImage: "lock.shield") .labelStyle(RoomTimelineViewLabelStyle()) } } @@ -66,7 +66,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider { private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem { EncryptedRoomTimelineItem(id: UUID().uuidString, - text: text, + body: text, encryptionType: .unknown, timestamp: timestamp, groupState: .single, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift index b53abd23b..c64ca2968 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift @@ -25,7 +25,7 @@ struct FileRoomTimelineView: View { HStack { Image(systemName: "doc.text.fill") .foregroundColor(.element.primaryContent) - FormattedBodyText(text: timelineItem.text) + FormattedBodyText(text: timelineItem.body) } .padding(.vertical, 12) .padding(.horizontal, 6) @@ -44,7 +44,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider { static var body: some View { VStack(spacing: 20.0) { FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString, - text: "document.pdf", + body: "document.pdf", timestamp: "Now", groupState: .single, isOutgoing: false, @@ -54,7 +54,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider { thumbnailSource: nil)) FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString, - text: "document.docx", + body: "document.docx", timestamp: "Now", groupState: .single, isOutgoing: false, @@ -64,7 +64,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider { thumbnailSource: nil)) FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString, - text: "document.txt", + body: "document.txt", timestamp: "Now", groupState: .single, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift index 53eff4b2a..e4a0850b9 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift @@ -76,7 +76,11 @@ struct FormattedBodyTextBubbleLayout: Layout { struct FormattedBodyText: View { @Environment(\.timelineStyle) private var timelineStyle - let attributedComponents: [AttributedStringBuilderComponent] + private let attributedComponents: [AttributedStringBuilderComponent] + + init(attributedString: AttributedString) { + attributedComponents = attributedString.formattedComponents + } var body: some View { if timelineStyle == .bubbles { @@ -155,7 +159,7 @@ struct FormattedBodyText: View { extension FormattedBodyText { init(text: String) { - attributedComponents = [.init(attributedString: AttributedString(text), isBlockquote: false, isReply: false)] + self.init(attributedString: AttributedString(text)) } } @@ -204,10 +208,8 @@ struct FormattedBodyText_Previews: PreviewProvider { VStack(alignment: .leading, spacing: 24.0) { ForEach(htmlStrings, id: \.self) { htmlString in - let attributedString = attributedStringBuilder.fromHTML(htmlString) - - if let components = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) { - FormattedBodyText(attributedComponents: components) + if let attributedString = attributedStringBuilder.fromHTML(htmlString) { + FormattedBodyText(attributedString: attributedString) .previewBubble() } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift index 124ab160b..fa1d2af7a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift @@ -72,7 +72,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider { static var body: some View { VStack(spacing: 20.0) { ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString, - text: "Some image", + body: "Some image", timestamp: "Now", groupState: .single, isOutgoing: false, @@ -81,7 +81,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider { source: nil)) ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString, - text: "Some other image", + body: "Some other image", timestamp: "Now", groupState: .single, isOutgoing: false, @@ -90,7 +90,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider { source: nil)) ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString, - text: "Blurhashed image", + body: "Blurhashed image", timestamp: "Now", groupState: .single, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift index 0c3b32a1a..e7bb59089 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift @@ -26,14 +26,15 @@ struct NoticeRoomTimelineView: View { // adds additional padding so the spacing between the icon and text is inconsistent. // Spacing: 6 = label spacing - formatted text padding - HStack(alignment: .firstTextBaseline, spacing: 6) { + + HStack(alignment: .firstTextBaseline, spacing: 6.0) { Image(systemName: "info.bubble").padding(.top, 2.0) .foregroundColor(.element.secondaryContent) - if let attributedComponents = timelineItem.attributedComponents { - FormattedBodyText(attributedComponents: attributedComponents) + if let attributedString = timelineItem.formattedBody { + FormattedBodyText(attributedString: attributedString) } else { - FormattedBodyText(text: timelineItem.text) + FormattedBodyText(text: timelineItem.body) } } .padding(.leading, 4) // Trailing padding is provided by FormattedBodyText @@ -63,7 +64,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider { private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem { NoticeRoomTimelineItem(id: UUID().uuidString, - text: text, + body: text, timestamp: timestamp, groupState: .single, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift index 1c8d1b281..c8c16b442 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift @@ -43,7 +43,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider { VStack(alignment: .leading, spacing: 0) { RoomTimelineViewProvider.separator(.init(text: "Today")) RoomTimelineViewProvider.text(.init(id: "", - text: "This is another message", + body: "This is another message", timestamp: "", groupState: .single, isOutgoing: true, @@ -54,7 +54,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider { RoomTimelineViewProvider.separator(.init(text: "Today")) RoomTimelineViewProvider.text(.init(id: "", - text: "This is a message", + body: "This is a message", timestamp: "", groupState: .single, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift index 07ac8760d..79b60a926 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift @@ -22,7 +22,7 @@ struct RedactedRoomTimelineView: View { var body: some View { TimelineStyler(timelineItem: timelineItem) { - Label(timelineItem.text, systemImage: "trash") + Label(timelineItem.body, systemImage: "trash") .labelStyle(RoomTimelineViewLabelStyle()) .imageScale(.small) // Smaller icon so that the bubble remains rounded on the outside. } @@ -43,7 +43,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider { private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem { RedactedRoomTimelineItem(id: UUID().uuidString, - text: text, + body: text, timestamp: timestamp, groupState: .single, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift index 6fd2dcd6b..a02e4d6e3 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift @@ -20,7 +20,7 @@ struct StateRoomTimelineView: View { let timelineItem: StateRoomTimelineItem var body: some View { - Text(timelineItem.text) + Text(timelineItem.body) .font(.element.footnote) .multilineTextAlignment(.center) .foregroundColor(.element.secondaryContent) @@ -41,7 +41,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider { } static let item = StateRoomTimelineItem(id: UUID().uuidString, - text: "Alice joined", + body: "Alice joined", timestamp: "Now", groupState: .beginning, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift index 000c4daa3..224899ce7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift @@ -31,7 +31,7 @@ struct StickerRoomTimelineView: View { .frame(maxHeight: 300) .aspectRatio(timelineItem.aspectRatio, contentMode: .fit) } - .accessibilityLabel(timelineItem.text) + .accessibilityLabel(timelineItem.body) } private var placeholder: some View { @@ -57,7 +57,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider { static var body: some View { VStack(spacing: 20.0) { StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, - text: "Some image", + body: "Some image", timestamp: "Now", groupState: .single, isOutgoing: false, @@ -66,7 +66,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider { imageURL: URL.picturesDirectory)) StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, - text: "Some other image", + body: "Some other image", timestamp: "Now", groupState: .single, isOutgoing: false, @@ -75,7 +75,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider { imageURL: URL.picturesDirectory)) StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString, - text: "Blurhashed image", + body: "Blurhashed image", timestamp: "Now", groupState: .single, isOutgoing: false, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift index cc202b0d6..97ec154fa 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift @@ -22,10 +22,10 @@ struct TextRoomTimelineView: View { var body: some View { TimelineStyler(timelineItem: timelineItem) { - if let attributedComponents = timelineItem.attributedComponents { - FormattedBodyText(attributedComponents: attributedComponents) + if let attributedString = timelineItem.formattedBody { + FormattedBodyText(attributedString: attributedString) } else { - FormattedBodyText(text: timelineItem.text) + FormattedBodyText(text: timelineItem.body) } } } @@ -65,7 +65,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider { private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem { TextRoomTimelineItem(id: UUID().uuidString, - text: text, + body: text, timestamp: timestamp, groupState: .single, isOutgoing: isOutgoing, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift index 793d0dd6f..c2d60e400 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift @@ -23,7 +23,7 @@ struct UnsupportedRoomTimelineView: View { TimelineStyler(timelineItem: timelineItem) { Label { VStack(alignment: .leading) { - Text("\(timelineItem.text): \(timelineItem.eventType)") + Text("\(timelineItem.body): \(timelineItem.eventType)") .fixedSize(horizontal: false, vertical: true) Text(timelineItem.error) @@ -62,7 +62,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider { private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem { UnsupportedRoomTimelineItem(id: UUID().uuidString, - text: text, + body: text, eventType: "Some Event Type", error: "Something went wrong", timestamp: timestamp, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift index b0b9c9b13..5c6463989 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift @@ -67,7 +67,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider { static var body: some View { VStack(spacing: 20.0) { VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, - text: "Some video", + body: "Some video", timestamp: "Now", groupState: .single, isOutgoing: false, @@ -78,7 +78,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider { thumbnailSource: nil)) VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, - text: "Some other video", + body: "Some other video", timestamp: "Now", groupState: .single, isOutgoing: false, @@ -89,7 +89,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider { thumbnailSource: nil)) VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString, - text: "Blurhashed video", + body: "Blurhashed video", timestamp: "Now", groupState: .single, isOutgoing: false, diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index 7651d5220..01dbdae62 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -21,7 +21,7 @@ enum RoomTimelineItemFixtures { static var `default`: [RoomTimelineItemProtocol] = [ SeparatorRoomTimelineItem(text: "Yesterday"), TextRoomTimelineItem(id: UUID().uuidString, - text: "That looks so good!", + body: "That looks so good!", timestamp: "10:10 AM", groupState: .single, isOutgoing: false, @@ -29,7 +29,7 @@ enum RoomTimelineItemFixtures { sender: .init(id: "", displayName: "Jacob"), properties: RoomTimelineItemProperties(isEdited: true)), TextRoomTimelineItem(id: UUID().uuidString, - text: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗", + body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗", timestamp: "10:11 AM", groupState: .beginning, isOutgoing: false, @@ -39,7 +39,7 @@ enum RoomTimelineItemFixtures { AggregatedReaction(key: "🙌", count: 1, isHighlighted: true) ])), TextRoomTimelineItem(id: UUID().uuidString, - text: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/", + body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/", timestamp: "10:11 AM", groupState: .end, isOutgoing: false, @@ -51,21 +51,21 @@ enum RoomTimelineItemFixtures { ])), SeparatorRoomTimelineItem(text: "Today"), TextRoomTimelineItem(id: UUID().uuidString, - text: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!", + body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!", timestamp: "5 PM", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "", displayName: "Helena")), TextRoomTimelineItem(id: UUID().uuidString, - text: "And John's speech was amazing!", + body: "And John's speech was amazing!", timestamp: "5 PM", groupState: .beginning, isOutgoing: true, isEditable: true, sender: .init(id: "", displayName: "Bob")), TextRoomTimelineItem(id: UUID().uuidString, - text: "New home office set up!", + body: "New home office set up!", timestamp: "5 PM", groupState: .end, isOutgoing: true, @@ -76,12 +76,8 @@ enum RoomTimelineItemFixtures { AggregatedReaction(key: "😁", count: 3, isHighlighted: false) ])), TextRoomTimelineItem(id: UUID().uuidString, - text: "", - attributedComponents: [ - AttributedStringBuilderComponent(attributedString: "Hol' up", isBlockquote: false, isReply: false), - AttributedStringBuilderComponent(attributedString: "New home office set up!", isBlockquote: true, isReply: false), - AttributedStringBuilderComponent(attributedString: "That's amazing! Congrats 🥳", isBlockquote: false, isReply: false) - ], + body: "", + formattedBody: AttributedStringBuilder().fromHTML("Hol' up
New home office set up!
That's amazing! Congrats 🥳"), timestamp: "5 PM", groupState: .single, isOutgoing: false, @@ -202,7 +198,7 @@ enum RoomTimelineItemFixtures { private extension TextRoomTimelineItem { init(text: String, groupState: TimelineItemGroupState = .single, senderDisplayName: String) { self.init(id: UUID().uuidString, - text: text, + body: text, timestamp: "10:47 am", groupState: groupState, isOutgoing: senderDisplayName == "Alice", diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index c26fe26cb..d7c0b16b4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -115,7 +115,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return .none } if let fileURL = item.cachedFileURL { - return .displayFile(fileURL: fileURL, title: item.text) + return .displayFile(fileURL: fileURL, title: item.body) } return .none case let item as VideoRoomTimelineItem: @@ -125,7 +125,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return .none } if let videoURL = item.cachedVideoURL { - return .displayVideo(videoURL: videoURL, title: item.text) + return .displayVideo(videoURL: videoURL, title: item.body) } return .none case let item as FileRoomTimelineItem: @@ -135,7 +135,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return .none } if let fileURL = item.cachedFileURL { - return .displayFile(fileURL: fileURL, title: item.text) + return .displayFile(fileURL: fileURL, title: item.body) } return .none default: @@ -337,7 +337,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return } - let fileExtension = movieFileExtension(for: timelineItem.text) + let fileExtension = movieFileExtension(for: timelineItem.body) switch await mediaProvider.loadFileFromSource(source, fileExtension: fileExtension) { case .success(let fileURL): guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }), @@ -383,7 +383,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } // This is not great. We could better estimate file extension from the mimetype. - guard let fileExtension = timelineItem.text.split(separator: ".").last else { + guard let fileExtension = timelineItem.body.split(separator: ".").last else { return } switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) { @@ -411,7 +411,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } // This is not great. We could better estimate file extension from the mimetype. - guard let fileExtension = timelineItem.text.split(separator: ".").last else { + guard let fileExtension = timelineItem.body.split(separator: ".").last else { return } switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index fd15b3e3d..244474815 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -34,7 +34,7 @@ enum TimelineItemGroupState: Hashable { } protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol { - var text: String { get } + var body: String { get } var timestamp: String { get } var shouldShowSenderDetails: Bool { get } var groupState: TimelineItemGroupState { get } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift index 6c291e9a3..56ed1ab76 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/EmoteRoomTimelineItem.swift @@ -18,8 +18,8 @@ import UIKit struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String - var attributedComponents: [AttributedStringBuilderComponent]? + let body: String + var formattedBody: AttributedString? let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift index b2f3be140..4c30a2ad5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/FileRoomTimelineItem.swift @@ -18,7 +18,7 @@ import UIKit struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String + let body: String let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift index 55f21e0f4..bc0070d94 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/ImageRoomTimelineItem.swift @@ -19,7 +19,7 @@ import UniformTypeIdentifiers struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String + let body: String let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift index 49033a2b1..353a3e132 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/NoticeRoomTimelineItem.swift @@ -18,8 +18,8 @@ import UIKit struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String - var attributedComponents: [AttributedStringBuilderComponent]? + let body: String + var formattedBody: AttributedString? let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift index 21f34c4f3..6a50de3bc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift @@ -18,8 +18,8 @@ import UIKit struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String - var attributedComponents: [AttributedStringBuilderComponent]? + let body: String + var formattedBody: AttributedString? let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift index f3b38b5e7..728e2f04f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift @@ -18,7 +18,7 @@ import UIKit struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String + let body: String let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift index 71f9bc5f9..012f58d59 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/EncryptedRoomTimelineItem.swift @@ -24,7 +24,7 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, } let id: String - let text: String + let body: String let encryptionType: EncryptionType let timestamp: String let groupState: TimelineItemGroupState diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/RedactedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/RedactedRoomTimelineItem.swift index 1cc302d61..16758292c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/RedactedRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/RedactedRoomTimelineItem.swift @@ -19,7 +19,7 @@ import UIKit struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String + let body: String let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift index 8b241fe94..9db011532 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StateRoomTimelineItem.swift @@ -18,7 +18,7 @@ import UIKit struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String + let body: String let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift index 9d1ff3382..59d1c632c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/StickerRoomTimelineItem.swift @@ -18,7 +18,7 @@ import UIKit struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String + let body: String let timestamp: String let groupState: TimelineItemGroupState let isOutgoing: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/UnsupportedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/UnsupportedRoomTimelineItem.swift index 3d0a04687..fc86f032c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/UnsupportedRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/UnsupportedRoomTimelineItem.swift @@ -18,7 +18,7 @@ import UIKit struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable { let id: String - let text: String + let body: String let eventType: String let error: String diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 233d3648b..57e2b5b10 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -98,7 +98,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { _ isOutgoing: Bool, _ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol { UnsupportedRoomTimelineItem(id: eventItemProxy.id, - text: ElementL10n.roomTimelineItemUnsupported, + body: ElementL10n.roomTimelineItemUnsupported, eventType: eventType, error: error, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), @@ -124,7 +124,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } return StickerRoomTimelineItem(id: eventItemProxy.id, - text: body, + body: body, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -154,7 +154,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } return EncryptedRoomTimelineItem(id: eventItemProxy.id, - text: ElementL10n.roomTimelineUnableToDecrypt, + body: ElementL10n.roomTimelineUnableToDecrypt, encryptionType: encryptionType, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, @@ -168,7 +168,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { _ isOutgoing: Bool, _ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol { RedactedRoomTimelineItem(id: eventItemProxy.id, - text: ElementL10n.eventRedacted, + body: ElementL10n.eventRedacted, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -180,12 +180,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy, _ isOutgoing: Bool, _ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol { - let attributedText = attributedStringBuilder.fromPlain(eventItemProxy.body) - let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText) + let formattedBody = attributedStringBuilder.fromPlain(eventItemProxy.body) return TextRoomTimelineItem(id: eventItemProxy.id, - text: eventItemProxy.body ?? "", - attributedComponents: attributedComponents, + body: eventItemProxy.body ?? "", + formattedBody: formattedBody, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -199,12 +198,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { _ message: MessageTimelineItem, _ isOutgoing: Bool, _ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol { - let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body)) - let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText) + let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body)) return TextRoomTimelineItem(id: message.id, - text: message.body, - attributedComponents: attributedComponents, + body: message.body, + formattedBody: formattedBody, timestamp: message.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -226,7 +224,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } return ImageRoomTimelineItem(id: message.id, - text: message.body, + body: message.body, timestamp: message.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -254,7 +252,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { } return VideoRoomTimelineItem(id: message.id, - text: message.body, + body: message.body, timestamp: message.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -277,7 +275,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { _ isOutgoing: Bool, _ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol { FileRoomTimelineItem(id: message.id, - text: message.body, + body: message.body, timestamp: message.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -294,12 +292,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { _ message: MessageTimelineItem, _ isOutgoing: Bool, _ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol { - let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body)) - let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText) + let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body)) return NoticeRoomTimelineItem(id: message.id, - text: message.body, - attributedComponents: attributedComponents, + body: message.body, + formattedBody: formattedBody, timestamp: message.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -316,18 +313,16 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { _ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol { let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id - var attributedText: AttributedString? + var formattedBody: AttributedString? if let htmlBody = message.htmlBody { - attributedText = attributedStringBuilder.fromHTML("* \(name) \(htmlBody)") + formattedBody = attributedStringBuilder.fromHTML("* \(name) \(htmlBody)") } else { - attributedText = attributedStringBuilder.fromPlain("* \(name) \(message.body)") + formattedBody = attributedStringBuilder.fromPlain("* \(name) \(message.body)") } - let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText) - return EmoteRoomTimelineItem(id: message.id, - text: message.body, - attributedComponents: attributedComponents, + body: message.body, + formattedBody: formattedBody, timestamp: message.timestamp.formatted(date: .omitted, time: .shortened), groupState: groupState, isOutgoing: isOutgoing, @@ -381,7 +376,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, isOutgoing: Bool) -> RoomTimelineItemProtocol { StateRoomTimelineItem(id: eventItemProxy.id, - text: text, + body: text, timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened), groupState: .single, isOutgoing: isOutgoing, diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index 02c76adfe..6aa125bc6 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -253,7 +253,7 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedString.runs.count, 1) - XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 1) + XCTAssertEqual(attributedString.formattedComponents.count, 1) for run in attributedString.runs where run.elementX.blockquote ?? false { return @@ -277,7 +277,7 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedString.runs.count, 3) - XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 3) + XCTAssertEqual(attributedString.formattedComponents.count, 3) for run in attributedString.runs where run.elementX.blockquote ?? false { return @@ -298,10 +298,8 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedString.runs.count, 3) - guard let coalescedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) else { - XCTFail("Could not build the attributed string components") - return - } + let coalescedComponents = attributedString.formattedComponents + XCTAssertEqual(coalescedComponents.count, 1) XCTAssertEqual(coalescedComponents.first?.attributedString.runs.count, 3, "Link not present in the component") @@ -322,10 +320,7 @@ class AttributedStringBuilderTests: XCTestCase { return } - guard let coalescedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) else { - XCTFail("Could not build the attributed string components") - return - } + let coalescedComponents = attributedString.formattedComponents XCTAssertEqual(coalescedComponents.count, 1) guard let component = coalescedComponents.first else { @@ -351,7 +346,7 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedString.runs.count, 7) - XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 1) + XCTAssertEqual(attributedString.formattedComponents.count, 1) var numberOfBlockquotes = 0 for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { @@ -378,10 +373,7 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedString.runs.count, 12) - guard let coalescedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) else { - XCTFail("Could not build the attributed string components") - return - } + let coalescedComponents = attributedString.formattedComponents XCTAssertEqual(coalescedComponents.count, 6) for component in coalescedComponents where component.isReply { diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index f91cd0922..20fac0238 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -210,30 +210,24 @@ class LoggingTests: XCTestCase { func testTimelineContentIsRedacted() throws { // Given timeline items that contain text let textAttributedString = "TextAttributed" - let textMessage = TextRoomTimelineItem(id: "mytextmessage", text: "TextString", - attributedComponents: [.init(attributedString: AttributedString(textAttributedString), - isBlockquote: false, - isReply: false)], + let textMessage = TextRoomTimelineItem(id: "mytextmessage", body: "TextString", + formattedBody: AttributedString(textAttributedString), timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender")) let noticeAttributedString = "NoticeAttributed" - let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", text: "NoticeString", - attributedComponents: [.init(attributedString: AttributedString(noticeAttributedString), - isBlockquote: false, - isReply: false)], + let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", body: "NoticeString", + formattedBody: AttributedString(noticeAttributedString), timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender")) let emoteAttributedString = "EmoteAttributed" - let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", text: "EmoteString", - attributedComponents: [.init(attributedString: AttributedString(emoteAttributedString), - isBlockquote: false, - isReply: false)], + let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", body: "EmoteString", + formattedBody: AttributedString(emoteAttributedString), timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender")) - let imageMessage = ImageRoomTimelineItem(id: "myimagemessage", text: "ImageString", + let imageMessage = ImageRoomTimelineItem(id: "myimagemessage", body: "ImageString", timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"), source: nil) - let videoMessage = VideoRoomTimelineItem(id: "myvideomessage", text: "VideoString", + let videoMessage = VideoRoomTimelineItem(id: "myvideomessage", body: "VideoString", timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"), duration: 0, source: nil, thumbnailSource: nil) - let fileMessage = FileRoomTimelineItem(id: "myfilemessage", text: "FileString", + let fileMessage = FileRoomTimelineItem(id: "myfilemessage", body: "FileString", timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"), source: nil, thumbnailSource: nil) @@ -258,25 +252,25 @@ class LoggingTests: XCTestCase { let content = try String(contentsOf: logFile) XCTAssertTrue(content.contains(textMessage.id)) - XCTAssertFalse(content.contains(textMessage.text)) + XCTAssertFalse(content.contains(textMessage.body)) XCTAssertFalse(content.contains(textAttributedString)) XCTAssertTrue(content.contains(noticeMessage.id)) - XCTAssertFalse(content.contains(noticeMessage.text)) + XCTAssertFalse(content.contains(noticeMessage.body)) XCTAssertFalse(content.contains(noticeAttributedString)) XCTAssertTrue(content.contains(emoteMessage.id)) - XCTAssertFalse(content.contains(emoteMessage.text)) + XCTAssertFalse(content.contains(emoteMessage.body)) XCTAssertFalse(content.contains(emoteAttributedString)) XCTAssertTrue(content.contains(imageMessage.id)) - XCTAssertFalse(content.contains(imageMessage.text)) + XCTAssertFalse(content.contains(imageMessage.body)) XCTAssertTrue(content.contains(videoMessage.id)) - XCTAssertFalse(content.contains(videoMessage.text)) + XCTAssertFalse(content.contains(videoMessage.body)) XCTAssertTrue(content.contains(fileMessage.id)) - XCTAssertFalse(content.contains(fileMessage.text)) + XCTAssertFalse(content.contains(fileMessage.body)) } func testRustMessageContentIsRedacted() throws { diff --git a/UnitTests/Sources/PermalinkBuilderTests.swift b/UnitTests/Sources/PermalinkBuilderTests.swift index d7145b593..8a2f36f24 100644 --- a/UnitTests/Sources/PermalinkBuilderTests.swift +++ b/UnitTests/Sources/PermalinkBuilderTests.swift @@ -106,4 +106,21 @@ class PermalinkBuilderTests: XCTestCase { XCTAssertEqual(error as? PermalinkBuilderError, PermalinkBuilderError.invalidEventIdentifier) } } + + func testPermalinkDetection() { + var url = URL(staticString: "https://www.matrix.org") + XCTAssertEqual(PermalinkBuilder.detectPermalink(in: url), nil) + + url = URL(staticString: "https://matrix.to/#/@bob:matrix.org?via=matrix.org") + XCTAssertEqual(PermalinkBuilder.detectPermalink(in: url), PermalinkType.userIdentifier("@bob:matrix.org")) + + url = URL(staticString: "https://matrix.to/#/!roomidentifier:matrix.org?via=matrix.org") + XCTAssertEqual(PermalinkBuilder.detectPermalink(in: url), PermalinkType.roomIdentifier("!roomidentifier:matrix.org")) + + url = URL(staticString: "https://matrix.to/#/%23roomalias:matrix.org?via=matrix.org") + XCTAssertEqual(PermalinkBuilder.detectPermalink(in: url), PermalinkType.roomAlias("#roomalias:matrix.org")) + + url = URL(staticString: "https://matrix.to/#/!roomidentifier:matrix.org/$eventidentifier?via=matrix.org") + XCTAssertEqual(PermalinkBuilder.detectPermalink(in: url), PermalinkType.event(roomIdentifier: "!roomidentifier:matrix.org", eventIdentifier: "$eventidentifier")) + } }