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:
@@ -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 */,
|
||||
|
||||
37
ElementX/Sources/Other/Extensions/AttributedString.swift
Normal file
37
ElementX/Sources/Other/Extensions/AttributedString.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,4 @@ protocol AttributedStringBuilderProtocol {
|
||||
func fromPlain(_ string: String?) -> AttributedString?
|
||||
|
||||
func fromHTML(_ htmlString: String?) -> AttributedString?
|
||||
|
||||
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user