Add permalink detection and custom attributed string attributes (#536)

* Pass full attributed string to TimelineItems and let the UI level handle blockquote coalescing
* Add permalink detection and attribute embedding within the attributed string
This commit is contained in:
Stefan Ceriu
2023-02-06 16:59:32 +02:00
committed by GitHub
parent 8cf55b993f
commit 1cac46b11e
38 changed files with 287 additions and 171 deletions

View File

@@ -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 = "<group>"; };
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreen.swift; sourceTree = "<group>"; };
529513218340CC8419273165 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
52BD6ED18E2EB61E28C340AD /* AttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedString.swift; sourceTree = "<group>"; };
52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = "<group>"; };
53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewAdapter.swift; sourceTree = "<group>"; };
534A5C8FCDE2CBC50266B9F2 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = gl; path = gl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -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 */,

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -26,6 +26,4 @@ protocol AttributedStringBuilderProtocol {
func fromPlain(_ string: String?) -> AttributedString?
func fromHTML(_ htmlString: String?) -> AttributedString?
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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()
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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: "Lets get lunch soon! New salad place opened up 🥗. When are yall free? 🤗",
body: "Lets get lunch soon! New salad place opened up 🥗. When are yall 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? Heres the menu, let me know what you want its on me!",
body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Heres the menu, let me know what you want its 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 <blockquote>New home office set up!</blockquote>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",

View File

@@ -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)) {

View File

@@ -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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<TextMessageContent>,
_ 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<NoticeMessageContent>,
_ 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,

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"))
}
}