mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
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 */; };
|
D63974A88CF2BC721F109C77 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; };
|
||||||
D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; };
|
D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; };
|
||||||
D79F0F852C6A4255D5E616D2 /* UserNotificationControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.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 */; };
|
D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; };
|
||||||
D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; };
|
D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; };
|
||||||
D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
534A5C8FCDE2CBC50266B9F2 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = gl; path = gl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
@@ -1460,6 +1462,7 @@
|
|||||||
44BBB96FAA2F0D53C507396B /* Extensions */ = {
|
44BBB96FAA2F0D53C507396B /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
52BD6ED18E2EB61E28C340AD /* AttributedString.swift */,
|
||||||
B6E89E530A8E92EC44301CA1 /* Bundle.swift */,
|
B6E89E530A8E92EC44301CA1 /* Bundle.swift */,
|
||||||
A9FAFE1C2149E6AC8156ED2B /* Collection.swift */,
|
A9FAFE1C2149E6AC8156ED2B /* Collection.swift */,
|
||||||
2141693488CE5446BB391964 /* Date.swift */,
|
2141693488CE5446BB391964 /* Date.swift */,
|
||||||
@@ -3037,6 +3040,7 @@
|
|||||||
9462C62798F47E39DCC182D2 /* Application.swift in Sources */,
|
9462C62798F47E39DCC182D2 /* Application.swift in Sources */,
|
||||||
74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */,
|
74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */,
|
||||||
90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */,
|
90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */,
|
||||||
|
D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */,
|
||||||
3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */,
|
3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */,
|
||||||
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */,
|
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */,
|
||||||
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */,
|
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */,
|
||||||
|
|||||||
@@ -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)
|
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||||
removeDefaultForegroundColor(mutableAttributedString)
|
removeDefaultForegroundColor(mutableAttributedString)
|
||||||
addLinks(mutableAttributedString)
|
addLinks(mutableAttributedString)
|
||||||
|
detectPermalinks(mutableAttributedString)
|
||||||
removeLinkColors(mutableAttributedString)
|
removeLinkColors(mutableAttributedString)
|
||||||
replaceMarkedBlockquotes(mutableAttributedString)
|
replaceMarkedBlockquotes(mutableAttributedString)
|
||||||
replaceMarkedCodeBlocks(mutableAttributedString)
|
replaceMarkedCodeBlocks(mutableAttributedString)
|
||||||
@@ -80,28 +81,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
return try? AttributedString(mutableAttributedString, including: \.elementX)
|
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
|
// MARK: - Private
|
||||||
|
|
||||||
private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) {
|
private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) {
|
||||||
@@ -115,7 +94,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
return
|
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
|
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.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) {
|
private func removeDefaultForegroundColor(_ attributedString: NSMutableAttributedString) {
|
||||||
attributedString.enumerateAttribute(.foregroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
|
attributedString.enumerateAttribute(.foregroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
|
||||||
if value as? UIColor == UIColor.black {
|
if value as? UIColor == UIColor.black {
|
||||||
@@ -241,5 +241,9 @@ extension UIColor {
|
|||||||
|
|
||||||
extension NSAttributedString.Key {
|
extension NSAttributedString.Key {
|
||||||
static let DTTextBlocks: NSAttributedString.Key = .init(rawValue: DTTextBlocksAttribute)
|
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 fromPlain(_ string: String?) -> AttributedString?
|
||||||
|
|
||||||
func fromHTML(_ htmlString: String?) -> AttributedString?
|
func fromHTML(_ htmlString: String?) -> AttributedString?
|
||||||
|
|
||||||
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,40 @@ enum BlockquoteAttribute: AttributedStringKey {
|
|||||||
public static var name = "MXBlockquoteAttribute"
|
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 {
|
extension AttributeScopes {
|
||||||
struct ElementXAttributes: AttributeScope {
|
struct ElementXAttributes: AttributeScope {
|
||||||
let blockquote: BlockquoteAttribute
|
let blockquote: BlockquoteAttribute
|
||||||
|
|
||||||
|
let userID: UserIDAttribute
|
||||||
|
let roomID: RoomIDAttribute
|
||||||
|
let roomAlias: RoomAliasAttribute
|
||||||
|
let eventID: EventIDAttribute
|
||||||
|
|
||||||
let swiftUI: SwiftUIAttributes
|
let swiftUI: SwiftUIAttributes
|
||||||
let uiKit: UIKitAttributes
|
let uiKit: UIKitAttributes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,59 @@ enum PermalinkBuilderError: Error {
|
|||||||
case failedAddingPercentEncoding
|
case failedAddingPercentEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PermalinkType: Equatable {
|
||||||
|
case userIdentifier(String)
|
||||||
|
case roomIdentifier(String)
|
||||||
|
case roomAlias(String)
|
||||||
|
case event(roomIdentifier: String, eventIdentifier: String)
|
||||||
|
}
|
||||||
|
|
||||||
enum PermalinkBuilder {
|
enum PermalinkBuilder {
|
||||||
static var uriComponentCharacterSet: CharacterSet = {
|
private static var uriComponentCharacterSet: CharacterSet = {
|
||||||
var charset = CharacterSet.alphanumerics
|
var charset = CharacterSet.alphanumerics
|
||||||
charset.insert(charactersIn: "-_.!~*'()")
|
charset.insert(charactersIn: "-_.!~*'()")
|
||||||
return charset
|
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 {
|
static func permalinkTo(userIdentifier: String) throws -> URL {
|
||||||
guard MatrixEntityRegex.isMatrixUserIdentifier(userIdentifier) else {
|
guard MatrixEntityRegex.isMatrixUserIdentifier(userIdentifier) else {
|
||||||
throw PermalinkBuilderError.invalidUserIdentifier
|
throw PermalinkBuilderError.invalidUserIdentifier
|
||||||
|
|||||||
@@ -245,14 +245,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
case .react:
|
case .react:
|
||||||
callback?(.displayEmojiPicker(itemId: item.id))
|
callback?(.displayEmojiPicker(itemId: item.id))
|
||||||
case .copy:
|
case .copy:
|
||||||
UIPasteboard.general.string = item.text
|
UIPasteboard.general.string = item.body
|
||||||
case .edit:
|
case .edit:
|
||||||
state.bindings.composerFocused = true
|
state.bindings.composerFocused = true
|
||||||
state.bindings.composerText = item.text
|
state.bindings.composerText = item.body
|
||||||
state.composerMode = .edit(originalItemId: item.id)
|
state.composerMode = .edit(originalItemId: item.id)
|
||||||
case .quote:
|
case .quote:
|
||||||
state.bindings.composerFocused = true
|
state.bindings.composerFocused = true
|
||||||
state.bindings.composerText = "> \(item.text)"
|
state.bindings.composerText = "> \(item.body)"
|
||||||
case .copyPermalink:
|
case .copyPermalink:
|
||||||
do {
|
do {
|
||||||
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomID)
|
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomID)
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ struct EmoteRoomTimelineView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
if let attributedComponents = timelineItem.attributedComponents {
|
if let attributedString = timelineItem.formattedBody {
|
||||||
FormattedBodyText(attributedComponents: attributedComponents)
|
FormattedBodyText(attributedString: attributedString)
|
||||||
} else {
|
} 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 {
|
private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem {
|
||||||
EmoteRoomTimelineItem(id: UUID().uuidString,
|
EmoteRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: text,
|
body: text,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct EncryptedRoomTimelineView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
Label(timelineItem.text, systemImage: "lock.shield")
|
Label(timelineItem.body, systemImage: "lock.shield")
|
||||||
.labelStyle(RoomTimelineViewLabelStyle())
|
.labelStyle(RoomTimelineViewLabelStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
|
|||||||
|
|
||||||
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem {
|
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem {
|
||||||
EncryptedRoomTimelineItem(id: UUID().uuidString,
|
EncryptedRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: text,
|
body: text,
|
||||||
encryptionType: .unknown,
|
encryptionType: .unknown,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct FileRoomTimelineView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "doc.text.fill")
|
Image(systemName: "doc.text.fill")
|
||||||
.foregroundColor(.element.primaryContent)
|
.foregroundColor(.element.primaryContent)
|
||||||
FormattedBodyText(text: timelineItem.text)
|
FormattedBodyText(text: timelineItem.body)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
@@ -44,7 +44,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
static var body: some View {
|
static var body: some View {
|
||||||
VStack(spacing: 20.0) {
|
VStack(spacing: 20.0) {
|
||||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "document.pdf",
|
body: "document.pdf",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -54,7 +54,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
thumbnailSource: nil))
|
thumbnailSource: nil))
|
||||||
|
|
||||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "document.docx",
|
body: "document.docx",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -64,7 +64,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
thumbnailSource: nil))
|
thumbnailSource: nil))
|
||||||
|
|
||||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "document.txt",
|
body: "document.txt",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -76,7 +76,11 @@ struct FormattedBodyTextBubbleLayout: Layout {
|
|||||||
struct FormattedBodyText: View {
|
struct FormattedBodyText: View {
|
||||||
@Environment(\.timelineStyle) private var timelineStyle
|
@Environment(\.timelineStyle) private var timelineStyle
|
||||||
|
|
||||||
let attributedComponents: [AttributedStringBuilderComponent]
|
private let attributedComponents: [AttributedStringBuilderComponent]
|
||||||
|
|
||||||
|
init(attributedString: AttributedString) {
|
||||||
|
attributedComponents = attributedString.formattedComponents
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if timelineStyle == .bubbles {
|
if timelineStyle == .bubbles {
|
||||||
@@ -155,7 +159,7 @@ struct FormattedBodyText: View {
|
|||||||
|
|
||||||
extension FormattedBodyText {
|
extension FormattedBodyText {
|
||||||
init(text: String) {
|
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) {
|
VStack(alignment: .leading, spacing: 24.0) {
|
||||||
ForEach(htmlStrings, id: \.self) { htmlString in
|
ForEach(htmlStrings, id: \.self) { htmlString in
|
||||||
let attributedString = attributedStringBuilder.fromHTML(htmlString)
|
if let attributedString = attributedStringBuilder.fromHTML(htmlString) {
|
||||||
|
FormattedBodyText(attributedString: attributedString)
|
||||||
if let components = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) {
|
|
||||||
FormattedBodyText(attributedComponents: components)
|
|
||||||
.previewBubble()
|
.previewBubble()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
static var body: some View {
|
static var body: some View {
|
||||||
VStack(spacing: 20.0) {
|
VStack(spacing: 20.0) {
|
||||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Some image",
|
body: "Some image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -81,7 +81,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
source: nil))
|
source: nil))
|
||||||
|
|
||||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Some other image",
|
body: "Some other image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -90,7 +90,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
source: nil))
|
source: nil))
|
||||||
|
|
||||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Blurhashed image",
|
body: "Blurhashed image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -26,14 +26,15 @@ struct NoticeRoomTimelineView: View {
|
|||||||
// adds additional padding so the spacing between the icon and text is inconsistent.
|
// adds additional padding so the spacing between the icon and text is inconsistent.
|
||||||
|
|
||||||
// Spacing: 6 = label spacing - formatted text padding
|
// 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)
|
Image(systemName: "info.bubble").padding(.top, 2.0)
|
||||||
.foregroundColor(.element.secondaryContent)
|
.foregroundColor(.element.secondaryContent)
|
||||||
|
|
||||||
if let attributedComponents = timelineItem.attributedComponents {
|
if let attributedString = timelineItem.formattedBody {
|
||||||
FormattedBodyText(attributedComponents: attributedComponents)
|
FormattedBodyText(attributedString: attributedString)
|
||||||
} else {
|
} else {
|
||||||
FormattedBodyText(text: timelineItem.text)
|
FormattedBodyText(text: timelineItem.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading, 4) // Trailing padding is provided by FormattedBodyText
|
.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 {
|
private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem {
|
||||||
NoticeRoomTimelineItem(id: UUID().uuidString,
|
NoticeRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: text,
|
body: text,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
RoomTimelineViewProvider.separator(.init(text: "Today"))
|
RoomTimelineViewProvider.separator(.init(text: "Today"))
|
||||||
RoomTimelineViewProvider.text(.init(id: "",
|
RoomTimelineViewProvider.text(.init(id: "",
|
||||||
text: "This is another message",
|
body: "This is another message",
|
||||||
timestamp: "",
|
timestamp: "",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
@@ -54,7 +54,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
|
|
||||||
RoomTimelineViewProvider.separator(.init(text: "Today"))
|
RoomTimelineViewProvider.separator(.init(text: "Today"))
|
||||||
RoomTimelineViewProvider.text(.init(id: "",
|
RoomTimelineViewProvider.text(.init(id: "",
|
||||||
text: "This is a message",
|
body: "This is a message",
|
||||||
timestamp: "",
|
timestamp: "",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct RedactedRoomTimelineView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
Label(timelineItem.text, systemImage: "trash")
|
Label(timelineItem.body, systemImage: "trash")
|
||||||
.labelStyle(RoomTimelineViewLabelStyle())
|
.labelStyle(RoomTimelineViewLabelStyle())
|
||||||
.imageScale(.small) // Smaller icon so that the bubble remains rounded on the outside.
|
.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 {
|
private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem {
|
||||||
RedactedRoomTimelineItem(id: UUID().uuidString,
|
RedactedRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: text,
|
body: text,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ struct StateRoomTimelineView: View {
|
|||||||
let timelineItem: StateRoomTimelineItem
|
let timelineItem: StateRoomTimelineItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(timelineItem.text)
|
Text(timelineItem.body)
|
||||||
.font(.element.footnote)
|
.font(.element.footnote)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundColor(.element.secondaryContent)
|
.foregroundColor(.element.secondaryContent)
|
||||||
@@ -41,7 +41,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static let item = StateRoomTimelineItem(id: UUID().uuidString,
|
static let item = StateRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Alice joined",
|
body: "Alice joined",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .beginning,
|
groupState: .beginning,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct StickerRoomTimelineView: View {
|
|||||||
.frame(maxHeight: 300)
|
.frame(maxHeight: 300)
|
||||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(timelineItem.text)
|
.accessibilityLabel(timelineItem.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var placeholder: some View {
|
private var placeholder: some View {
|
||||||
@@ -57,7 +57,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
static var body: some View {
|
static var body: some View {
|
||||||
VStack(spacing: 20.0) {
|
VStack(spacing: 20.0) {
|
||||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Some image",
|
body: "Some image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -66,7 +66,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
imageURL: URL.picturesDirectory))
|
imageURL: URL.picturesDirectory))
|
||||||
|
|
||||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Some other image",
|
body: "Some other image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -75,7 +75,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
imageURL: URL.picturesDirectory))
|
imageURL: URL.picturesDirectory))
|
||||||
|
|
||||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Blurhashed image",
|
body: "Blurhashed image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ struct TextRoomTimelineView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
if let attributedComponents = timelineItem.attributedComponents {
|
if let attributedString = timelineItem.formattedBody {
|
||||||
FormattedBodyText(attributedComponents: attributedComponents)
|
FormattedBodyText(attributedString: attributedString)
|
||||||
} else {
|
} 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 {
|
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: text,
|
body: text,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct UnsupportedRoomTimelineView: View {
|
|||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
Label {
|
Label {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("\(timelineItem.text): \(timelineItem.eventType)")
|
Text("\(timelineItem.body): \(timelineItem.eventType)")
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
Text(timelineItem.error)
|
Text(timelineItem.error)
|
||||||
@@ -62,7 +62,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
|
|||||||
|
|
||||||
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem {
|
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem {
|
||||||
UnsupportedRoomTimelineItem(id: UUID().uuidString,
|
UnsupportedRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: text,
|
body: text,
|
||||||
eventType: "Some Event Type",
|
eventType: "Some Event Type",
|
||||||
error: "Something went wrong",
|
error: "Something went wrong",
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
static var body: some View {
|
static var body: some View {
|
||||||
VStack(spacing: 20.0) {
|
VStack(spacing: 20.0) {
|
||||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Some video",
|
body: "Some video",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -78,7 +78,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
thumbnailSource: nil))
|
thumbnailSource: nil))
|
||||||
|
|
||||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Some other video",
|
body: "Some other video",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -89,7 +89,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
thumbnailSource: nil))
|
thumbnailSource: nil))
|
||||||
|
|
||||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "Blurhashed video",
|
body: "Blurhashed video",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
static var `default`: [RoomTimelineItemProtocol] = [
|
static var `default`: [RoomTimelineItemProtocol] = [
|
||||||
SeparatorRoomTimelineItem(text: "Yesterday"),
|
SeparatorRoomTimelineItem(text: "Yesterday"),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "That looks so good!",
|
body: "That looks so good!",
|
||||||
timestamp: "10:10 AM",
|
timestamp: "10:10 AM",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -29,7 +29,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
sender: .init(id: "", displayName: "Jacob"),
|
sender: .init(id: "", displayName: "Jacob"),
|
||||||
properties: RoomTimelineItemProperties(isEdited: true)),
|
properties: RoomTimelineItemProperties(isEdited: true)),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
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",
|
timestamp: "10:11 AM",
|
||||||
groupState: .beginning,
|
groupState: .beginning,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -39,7 +39,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
|
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
|
||||||
])),
|
])),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
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",
|
timestamp: "10:11 AM",
|
||||||
groupState: .end,
|
groupState: .end,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -51,21 +51,21 @@ enum RoomTimelineItemFixtures {
|
|||||||
])),
|
])),
|
||||||
SeparatorRoomTimelineItem(text: "Today"),
|
SeparatorRoomTimelineItem(text: "Today"),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
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",
|
timestamp: "5 PM",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "", displayName: "Helena")),
|
sender: .init(id: "", displayName: "Helena")),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "And John's speech was amazing!",
|
body: "And John's speech was amazing!",
|
||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
groupState: .beginning,
|
groupState: .beginning,
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
sender: .init(id: "", displayName: "Bob")),
|
sender: .init(id: "", displayName: "Bob")),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "New home office set up!",
|
body: "New home office set up!",
|
||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
groupState: .end,
|
groupState: .end,
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
@@ -76,12 +76,8 @@ enum RoomTimelineItemFixtures {
|
|||||||
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
|
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
|
||||||
])),
|
])),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
text: "",
|
body: "",
|
||||||
attributedComponents: [
|
formattedBody: AttributedStringBuilder().fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳"),
|
||||||
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)
|
|
||||||
],
|
|
||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
@@ -202,7 +198,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
private extension TextRoomTimelineItem {
|
private extension TextRoomTimelineItem {
|
||||||
init(text: String, groupState: TimelineItemGroupState = .single, senderDisplayName: String) {
|
init(text: String, groupState: TimelineItemGroupState = .single, senderDisplayName: String) {
|
||||||
self.init(id: UUID().uuidString,
|
self.init(id: UUID().uuidString,
|
||||||
text: text,
|
body: text,
|
||||||
timestamp: "10:47 am",
|
timestamp: "10:47 am",
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: senderDisplayName == "Alice",
|
isOutgoing: senderDisplayName == "Alice",
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
if let fileURL = item.cachedFileURL {
|
if let fileURL = item.cachedFileURL {
|
||||||
return .displayFile(fileURL: fileURL, title: item.text)
|
return .displayFile(fileURL: fileURL, title: item.body)
|
||||||
}
|
}
|
||||||
return .none
|
return .none
|
||||||
case let item as VideoRoomTimelineItem:
|
case let item as VideoRoomTimelineItem:
|
||||||
@@ -125,7 +125,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
if let videoURL = item.cachedVideoURL {
|
if let videoURL = item.cachedVideoURL {
|
||||||
return .displayVideo(videoURL: videoURL, title: item.text)
|
return .displayVideo(videoURL: videoURL, title: item.body)
|
||||||
}
|
}
|
||||||
return .none
|
return .none
|
||||||
case let item as FileRoomTimelineItem:
|
case let item as FileRoomTimelineItem:
|
||||||
@@ -135,7 +135,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
if let fileURL = item.cachedFileURL {
|
if let fileURL = item.cachedFileURL {
|
||||||
return .displayFile(fileURL: fileURL, title: item.text)
|
return .displayFile(fileURL: fileURL, title: item.body)
|
||||||
}
|
}
|
||||||
return .none
|
return .none
|
||||||
default:
|
default:
|
||||||
@@ -337,7 +337,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileExtension = movieFileExtension(for: timelineItem.text)
|
let fileExtension = movieFileExtension(for: timelineItem.body)
|
||||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: fileExtension) {
|
switch await mediaProvider.loadFileFromSource(source, fileExtension: fileExtension) {
|
||||||
case .success(let fileURL):
|
case .success(let fileURL):
|
||||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
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.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
|
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.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
|
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ enum TimelineItemGroupState: Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
|
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
|
||||||
var text: String { get }
|
var body: String { get }
|
||||||
var timestamp: String { get }
|
var timestamp: String { get }
|
||||||
var shouldShowSenderDetails: Bool { get }
|
var shouldShowSenderDetails: Bool { get }
|
||||||
var groupState: TimelineItemGroupState { get }
|
var groupState: TimelineItemGroupState { get }
|
||||||
|
|||||||
+2
-2
@@ -18,8 +18,8 @@ import UIKit
|
|||||||
|
|
||||||
struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
var formattedBody: AttributedString?
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import UIKit
|
|||||||
|
|
||||||
struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ import UniformTypeIdentifiers
|
|||||||
|
|
||||||
struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+2
-2
@@ -18,8 +18,8 @@ import UIKit
|
|||||||
|
|
||||||
struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
var formattedBody: AttributedString?
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+2
-2
@@ -18,8 +18,8 @@ import UIKit
|
|||||||
|
|
||||||
struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
var formattedBody: AttributedString?
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import UIKit
|
|||||||
|
|
||||||
struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable,
|
|||||||
}
|
}
|
||||||
|
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
let encryptionType: EncryptionType
|
let encryptionType: EncryptionType
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ import UIKit
|
|||||||
|
|
||||||
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import UIKit
|
|||||||
|
|
||||||
struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import UIKit
|
|||||||
|
|
||||||
struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
let groupState: TimelineItemGroupState
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import UIKit
|
|||||||
|
|
||||||
struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let text: String
|
let body: String
|
||||||
|
|
||||||
let eventType: String
|
let eventType: String
|
||||||
let error: String
|
let error: String
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool,
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||||
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
|
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
|
||||||
text: ElementL10n.roomTimelineItemUnsupported,
|
body: ElementL10n.roomTimelineItemUnsupported,
|
||||||
eventType: eventType,
|
eventType: eventType,
|
||||||
error: error,
|
error: error,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
@@ -124,7 +124,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return StickerRoomTimelineItem(id: eventItemProxy.id,
|
return StickerRoomTimelineItem(id: eventItemProxy.id,
|
||||||
text: body,
|
body: body,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -154,7 +154,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return EncryptedRoomTimelineItem(id: eventItemProxy.id,
|
return EncryptedRoomTimelineItem(id: eventItemProxy.id,
|
||||||
text: ElementL10n.roomTimelineUnableToDecrypt,
|
body: ElementL10n.roomTimelineUnableToDecrypt,
|
||||||
encryptionType: encryptionType,
|
encryptionType: encryptionType,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
@@ -168,7 +168,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool,
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||||
RedactedRoomTimelineItem(id: eventItemProxy.id,
|
RedactedRoomTimelineItem(id: eventItemProxy.id,
|
||||||
text: ElementL10n.eventRedacted,
|
body: ElementL10n.eventRedacted,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -180,12 +180,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool,
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||||
let attributedText = attributedStringBuilder.fromPlain(eventItemProxy.body)
|
let formattedBody = attributedStringBuilder.fromPlain(eventItemProxy.body)
|
||||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
|
||||||
|
|
||||||
return TextRoomTimelineItem(id: eventItemProxy.id,
|
return TextRoomTimelineItem(id: eventItemProxy.id,
|
||||||
text: eventItemProxy.body ?? "",
|
body: eventItemProxy.body ?? "",
|
||||||
attributedComponents: attributedComponents,
|
formattedBody: formattedBody,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -199,12 +198,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
_ message: MessageTimelineItem<TextMessageContent>,
|
_ message: MessageTimelineItem<TextMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool,
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
|
||||||
|
|
||||||
return TextRoomTimelineItem(id: message.id,
|
return TextRoomTimelineItem(id: message.id,
|
||||||
text: message.body,
|
body: message.body,
|
||||||
attributedComponents: attributedComponents,
|
formattedBody: formattedBody,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -226,7 +224,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ImageRoomTimelineItem(id: message.id,
|
return ImageRoomTimelineItem(id: message.id,
|
||||||
text: message.body,
|
body: message.body,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -254,7 +252,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return VideoRoomTimelineItem(id: message.id,
|
return VideoRoomTimelineItem(id: message.id,
|
||||||
text: message.body,
|
body: message.body,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -277,7 +275,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool,
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||||
FileRoomTimelineItem(id: message.id,
|
FileRoomTimelineItem(id: message.id,
|
||||||
text: message.body,
|
body: message.body,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -294,12 +292,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
_ message: MessageTimelineItem<NoticeMessageContent>,
|
_ message: MessageTimelineItem<NoticeMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool,
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
|
||||||
|
|
||||||
return NoticeRoomTimelineItem(id: message.id,
|
return NoticeRoomTimelineItem(id: message.id,
|
||||||
text: message.body,
|
body: message.body,
|
||||||
attributedComponents: attributedComponents,
|
formattedBody: formattedBody,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -316,18 +313,16 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||||
let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id
|
let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id
|
||||||
|
|
||||||
var attributedText: AttributedString?
|
var formattedBody: AttributedString?
|
||||||
if let htmlBody = message.htmlBody {
|
if let htmlBody = message.htmlBody {
|
||||||
attributedText = attributedStringBuilder.fromHTML("* \(name) \(htmlBody)")
|
formattedBody = attributedStringBuilder.fromHTML("* \(name) \(htmlBody)")
|
||||||
} else {
|
} else {
|
||||||
attributedText = attributedStringBuilder.fromPlain("* \(name) \(message.body)")
|
formattedBody = attributedStringBuilder.fromPlain("* \(name) \(message.body)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
|
||||||
|
|
||||||
return EmoteRoomTimelineItem(id: message.id,
|
return EmoteRoomTimelineItem(id: message.id,
|
||||||
text: message.body,
|
body: message.body,
|
||||||
attributedComponents: attributedComponents,
|
formattedBody: formattedBody,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
groupState: groupState,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
@@ -381,7 +376,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
StateRoomTimelineItem(id: eventItemProxy.id,
|
StateRoomTimelineItem(id: eventItemProxy.id,
|
||||||
text: text,
|
body: text,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: .single,
|
groupState: .single,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(attributedString.runs.count, 1)
|
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 {
|
for run in attributedString.runs where run.elementX.blockquote ?? false {
|
||||||
return
|
return
|
||||||
@@ -277,7 +277,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(attributedString.runs.count, 3)
|
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 {
|
for run in attributedString.runs where run.elementX.blockquote ?? false {
|
||||||
return
|
return
|
||||||
@@ -298,10 +298,8 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(attributedString.runs.count, 3)
|
XCTAssertEqual(attributedString.runs.count, 3)
|
||||||
|
|
||||||
guard let coalescedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) else {
|
let coalescedComponents = attributedString.formattedComponents
|
||||||
XCTFail("Could not build the attributed string components")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
XCTAssertEqual(coalescedComponents.count, 1)
|
XCTAssertEqual(coalescedComponents.count, 1)
|
||||||
|
|
||||||
XCTAssertEqual(coalescedComponents.first?.attributedString.runs.count, 3, "Link not present in the component")
|
XCTAssertEqual(coalescedComponents.first?.attributedString.runs.count, 3, "Link not present in the component")
|
||||||
@@ -322,10 +320,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let coalescedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) else {
|
let coalescedComponents = attributedString.formattedComponents
|
||||||
XCTFail("Could not build the attributed string components")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
XCTAssertEqual(coalescedComponents.count, 1)
|
XCTAssertEqual(coalescedComponents.count, 1)
|
||||||
|
|
||||||
guard let component = coalescedComponents.first else {
|
guard let component = coalescedComponents.first else {
|
||||||
@@ -351,7 +346,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(attributedString.runs.count, 7)
|
XCTAssertEqual(attributedString.runs.count, 7)
|
||||||
|
|
||||||
XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 1)
|
XCTAssertEqual(attributedString.formattedComponents.count, 1)
|
||||||
|
|
||||||
var numberOfBlockquotes = 0
|
var numberOfBlockquotes = 0
|
||||||
for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil {
|
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)
|
XCTAssertEqual(attributedString.runs.count, 12)
|
||||||
|
|
||||||
guard let coalescedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) else {
|
let coalescedComponents = attributedString.formattedComponents
|
||||||
XCTFail("Could not build the attributed string components")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
XCTAssertEqual(coalescedComponents.count, 6)
|
XCTAssertEqual(coalescedComponents.count, 6)
|
||||||
for component in coalescedComponents where component.isReply {
|
for component in coalescedComponents where component.isReply {
|
||||||
|
|||||||
@@ -210,30 +210,24 @@ class LoggingTests: XCTestCase {
|
|||||||
func testTimelineContentIsRedacted() throws {
|
func testTimelineContentIsRedacted() throws {
|
||||||
// Given timeline items that contain text
|
// Given timeline items that contain text
|
||||||
let textAttributedString = "TextAttributed"
|
let textAttributedString = "TextAttributed"
|
||||||
let textMessage = TextRoomTimelineItem(id: "mytextmessage", text: "TextString",
|
let textMessage = TextRoomTimelineItem(id: "mytextmessage", body: "TextString",
|
||||||
attributedComponents: [.init(attributedString: AttributedString(textAttributedString),
|
formattedBody: AttributedString(textAttributedString),
|
||||||
isBlockquote: false,
|
|
||||||
isReply: false)],
|
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
||||||
let noticeAttributedString = "NoticeAttributed"
|
let noticeAttributedString = "NoticeAttributed"
|
||||||
let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", text: "NoticeString",
|
let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", body: "NoticeString",
|
||||||
attributedComponents: [.init(attributedString: AttributedString(noticeAttributedString),
|
formattedBody: AttributedString(noticeAttributedString),
|
||||||
isBlockquote: false,
|
|
||||||
isReply: false)],
|
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
||||||
let emoteAttributedString = "EmoteAttributed"
|
let emoteAttributedString = "EmoteAttributed"
|
||||||
let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", text: "EmoteString",
|
let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", body: "EmoteString",
|
||||||
attributedComponents: [.init(attributedString: AttributedString(emoteAttributedString),
|
formattedBody: AttributedString(emoteAttributedString),
|
||||||
isBlockquote: false,
|
|
||||||
isReply: false)],
|
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
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,
|
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false,
|
||||||
sender: .init(id: "sender"), source: nil)
|
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,
|
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false,
|
||||||
sender: .init(id: "sender"), duration: 0, source: nil, thumbnailSource: nil)
|
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,
|
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false,
|
||||||
sender: .init(id: "sender"), source: nil, thumbnailSource: nil)
|
sender: .init(id: "sender"), source: nil, thumbnailSource: nil)
|
||||||
|
|
||||||
@@ -258,25 +252,25 @@ class LoggingTests: XCTestCase {
|
|||||||
|
|
||||||
let content = try String(contentsOf: logFile)
|
let content = try String(contentsOf: logFile)
|
||||||
XCTAssertTrue(content.contains(textMessage.id))
|
XCTAssertTrue(content.contains(textMessage.id))
|
||||||
XCTAssertFalse(content.contains(textMessage.text))
|
XCTAssertFalse(content.contains(textMessage.body))
|
||||||
XCTAssertFalse(content.contains(textAttributedString))
|
XCTAssertFalse(content.contains(textAttributedString))
|
||||||
|
|
||||||
XCTAssertTrue(content.contains(noticeMessage.id))
|
XCTAssertTrue(content.contains(noticeMessage.id))
|
||||||
XCTAssertFalse(content.contains(noticeMessage.text))
|
XCTAssertFalse(content.contains(noticeMessage.body))
|
||||||
XCTAssertFalse(content.contains(noticeAttributedString))
|
XCTAssertFalse(content.contains(noticeAttributedString))
|
||||||
|
|
||||||
XCTAssertTrue(content.contains(emoteMessage.id))
|
XCTAssertTrue(content.contains(emoteMessage.id))
|
||||||
XCTAssertFalse(content.contains(emoteMessage.text))
|
XCTAssertFalse(content.contains(emoteMessage.body))
|
||||||
XCTAssertFalse(content.contains(emoteAttributedString))
|
XCTAssertFalse(content.contains(emoteAttributedString))
|
||||||
|
|
||||||
XCTAssertTrue(content.contains(imageMessage.id))
|
XCTAssertTrue(content.contains(imageMessage.id))
|
||||||
XCTAssertFalse(content.contains(imageMessage.text))
|
XCTAssertFalse(content.contains(imageMessage.body))
|
||||||
|
|
||||||
XCTAssertTrue(content.contains(videoMessage.id))
|
XCTAssertTrue(content.contains(videoMessage.id))
|
||||||
XCTAssertFalse(content.contains(videoMessage.text))
|
XCTAssertFalse(content.contains(videoMessage.body))
|
||||||
|
|
||||||
XCTAssertTrue(content.contains(fileMessage.id))
|
XCTAssertTrue(content.contains(fileMessage.id))
|
||||||
XCTAssertFalse(content.contains(fileMessage.text))
|
XCTAssertFalse(content.contains(fileMessage.body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRustMessageContentIsRedacted() throws {
|
func testRustMessageContentIsRedacted() throws {
|
||||||
|
|||||||
@@ -106,4 +106,21 @@ class PermalinkBuilderTests: XCTestCase {
|
|||||||
XCTAssertEqual(error as? PermalinkBuilderError, PermalinkBuilderError.invalidEventIdentifier)
|
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