mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Reply rendering (#861)
* Cleanup how we use message timeline items in preparation for rich reply rendering * Expose reply details up to the UI level * Render replies in the timeline (textual for now). Remove now unused AttributedStringBuilderComponent isReply flag. * Move reply detail loading to when the item appears instead of when it's created. * Fix unit tests * Fetch event details also when the reply details are in the error state * Tweaks following code review * Localise emote bodies * Fix replies not loading sometimes, use the current event identifier instead of the one being replied to
This commit is contained in:
@@ -341,7 +341,6 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
}
|
||||
|
||||
XCTAssertTrue(component.isBlockquote, "The reply quote should be a blockquote.")
|
||||
XCTAssertTrue(component.isReply, "The reply quote should be detected as a reply")
|
||||
}
|
||||
|
||||
func testMultipleGroupedBlockquotes() {
|
||||
@@ -388,9 +387,6 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
let coalescedComponents = attributedString.formattedComponents
|
||||
|
||||
XCTAssertEqual(coalescedComponents.count, 6)
|
||||
for component in coalescedComponents where component.isReply {
|
||||
XCTFail("None of the blockquotes should be detected as replies.")
|
||||
}
|
||||
|
||||
var numberOfBlockquotes = 0
|
||||
for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil {
|
||||
|
||||
@@ -186,37 +186,55 @@ class LoggingTests: XCTestCase {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let content = try String(contentsOf: logFile)
|
||||
XCTAssertTrue(content.contains(roomSummary.id))
|
||||
XCTAssertFalse(content.contains(roomName))
|
||||
XCTAssertFalse(content.contains(lastMessage))
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
func testTimelineContentIsRedacted() throws {
|
||||
// Given timeline items that contain text
|
||||
let textAttributedString = "TextAttributed"
|
||||
let textMessage = TextRoomTimelineItem(id: "mytextmessage", body: "TextString",
|
||||
formattedBody: AttributedString(textAttributedString),
|
||||
timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
||||
let textMessage = TextRoomTimelineItem(id: "mytextmessage",
|
||||
timestamp: "",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "sender"),
|
||||
content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString)))
|
||||
let noticeAttributedString = "NoticeAttributed"
|
||||
let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", body: "NoticeString",
|
||||
formattedBody: AttributedString(noticeAttributedString),
|
||||
timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
||||
let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage",
|
||||
timestamp: "",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "sender"),
|
||||
content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString)))
|
||||
let emoteAttributedString = "EmoteAttributed"
|
||||
let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", body: "EmoteString",
|
||||
formattedBody: AttributedString(emoteAttributedString),
|
||||
timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
||||
let imageMessage = ImageRoomTimelineItem(id: "myimagemessage", body: "ImageString",
|
||||
timestamp: "", isOutgoing: false, isEditable: false,
|
||||
let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage",
|
||||
timestamp: "",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "sender"),
|
||||
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"))
|
||||
let videoMessage = VideoRoomTimelineItem(id: "myvideomessage", body: "VideoString",
|
||||
timestamp: "", isOutgoing: false, isEditable: false,
|
||||
sender: .init(id: "sender"), duration: 0, source: nil, thumbnailSource: nil)
|
||||
let fileMessage = FileRoomTimelineItem(id: "myfilemessage", body: "FileString",
|
||||
timestamp: "", isOutgoing: false, isEditable: false,
|
||||
sender: .init(id: "sender"), source: nil, thumbnailSource: nil)
|
||||
content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString)))
|
||||
let imageMessage = ImageRoomTimelineItem(id: "myimagemessage",
|
||||
timestamp: "",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "sender"),
|
||||
content: .init(body: "ImageString", source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/gif"), thumbnailSource: nil))
|
||||
let videoMessage = VideoRoomTimelineItem(id: "myvideomessage",
|
||||
timestamp: "",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "sender"),
|
||||
content: .init(body: "VideoString", duration: 0, source: nil, thumbnailSource: nil))
|
||||
let fileMessage = FileRoomTimelineItem(id: "myfilemessage",
|
||||
timestamp: "",
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "sender"),
|
||||
content: .init(body: "FileString", source: nil, thumbnailSource: nil, contentType: nil))
|
||||
|
||||
// When logging that value
|
||||
XCTAssert(MXLogger.logFiles.isEmpty)
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
|
||||
class MessageTimelineItemTests: XCTestCase {
|
||||
// MARK: Image
|
||||
|
||||
func testImageContentType() {
|
||||
let mimetype = "image/gif"
|
||||
let imageContent = ImageMessageContent(body: "amazing.gif",
|
||||
source: mediaSourceFromUrl(url: "mxc://doesnt/matter"),
|
||||
info: makeImageInfo(mimetype: mimetype))
|
||||
let messageItem = MockEventTimelineItem.message(from: imageContent)
|
||||
XCTAssertEqual(messageItem.contentType, .gif)
|
||||
}
|
||||
|
||||
func testImageContentTypeWithoutMimetype() {
|
||||
let imageContent = ImageMessageContent(body: "amazing.jpeg",
|
||||
source: mediaSourceFromUrl(url: "mxc://doesnt/matter"),
|
||||
info: makeImageInfo(mimetype: nil))
|
||||
let messageItem = MockEventTimelineItem.message(from: imageContent)
|
||||
XCTAssertEqual(messageItem.contentType, .jpeg)
|
||||
}
|
||||
|
||||
func makeImageInfo(mimetype: String?) -> ImageInfo {
|
||||
ImageInfo(height: nil,
|
||||
width: nil,
|
||||
mimetype: mimetype,
|
||||
size: nil,
|
||||
thumbnailInfo: nil,
|
||||
thumbnailSource: nil,
|
||||
blurhash: nil)
|
||||
}
|
||||
|
||||
// MARK: Video
|
||||
|
||||
func testVideoContentType() {
|
||||
let mimetype = "video/x-msvideo"
|
||||
let imageContent = VideoMessageContent(body: "amazing.avi",
|
||||
source: mediaSourceFromUrl(url: "mxc://doesnt/matter"),
|
||||
info: makeVideoInfo(mimetype: mimetype))
|
||||
let messageItem = MockEventTimelineItem.message(from: imageContent)
|
||||
XCTAssertEqual(messageItem.contentType, .avi)
|
||||
}
|
||||
|
||||
func testVideoContentTypeWithoutMimetype() {
|
||||
let imageContent = VideoMessageContent(body: "amazing.mp4",
|
||||
source: mediaSourceFromUrl(url: "mxc://doesnt/matter"),
|
||||
info: makeVideoInfo(mimetype: nil))
|
||||
let messageItem = MockEventTimelineItem.message(from: imageContent)
|
||||
XCTAssertEqual(messageItem.contentType, .mpeg4Movie)
|
||||
}
|
||||
|
||||
func makeVideoInfo(mimetype: String?) -> VideoInfo {
|
||||
VideoInfo(duration: nil,
|
||||
height: nil,
|
||||
width: nil,
|
||||
mimetype: mimetype,
|
||||
size: nil,
|
||||
thumbnailInfo: nil,
|
||||
thumbnailSource: nil,
|
||||
blurhash: nil)
|
||||
}
|
||||
|
||||
// MARK: Audio
|
||||
|
||||
func testAudioContentType() {
|
||||
let mimetype = "audio/mp3"
|
||||
let imageContent = AudioMessageContent(body: "amazing.mp3",
|
||||
source: mediaSourceFromUrl(url: "mxc://doesnt/matter"),
|
||||
info: makeAudioInfo(mimetype: mimetype))
|
||||
let messageItem = MockEventTimelineItem.message(from: imageContent)
|
||||
XCTAssertEqual(messageItem.contentType, .mp3)
|
||||
}
|
||||
|
||||
func testAudioContentTypeWithoutMimetype() {
|
||||
let imageContent = AudioMessageContent(body: "amazing.m4a",
|
||||
source: mediaSourceFromUrl(url: "mxc://doesnt/matter"),
|
||||
info: makeAudioInfo(mimetype: nil))
|
||||
let messageItem = MockEventTimelineItem.message(from: imageContent)
|
||||
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
XCTAssertTrue(messageItem.contentType!.conforms(to: .mpeg4Audio))
|
||||
}
|
||||
|
||||
func makeAudioInfo(mimetype: String?) -> AudioInfo {
|
||||
AudioInfo(duration: nil, size: nil, mimetype: mimetype)
|
||||
}
|
||||
|
||||
// MARK: File
|
||||
|
||||
func testFileContentType() {
|
||||
let mimetype = "text"
|
||||
let imageContent = FileMessageContent(body: "amazing.txt",
|
||||
source: mediaSourceFromUrl(url: "mxc://doesnt/matter"),
|
||||
info: makeFileInfo(mimetype: mimetype))
|
||||
let messageItem = MockEventTimelineItem.message(from: imageContent)
|
||||
XCTAssertEqual(messageItem.contentType, .plainText)
|
||||
}
|
||||
|
||||
func testFileContentTypeWithoutMimetype() {
|
||||
let imageContent = FileMessageContent(body: "amazing.rtf",
|
||||
source: mediaSourceFromUrl(url: "mxc://doesnt/matter"),
|
||||
info: makeFileInfo(mimetype: nil))
|
||||
let messageItem = MockEventTimelineItem.message(from: imageContent)
|
||||
XCTAssertEqual(messageItem.contentType, .rtf)
|
||||
}
|
||||
|
||||
func makeFileInfo(mimetype: String?) -> FileInfo {
|
||||
FileInfo(mimetype: mimetype, size: nil, thumbnailInfo: nil, thumbnailSource: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mocks
|
||||
|
||||
private struct MockEventTimelineItem: EventTimelineItemProtocol {
|
||||
static func message<Content: MessageContentProtocol>(from content: Content) -> MessageTimelineItem<Content> {
|
||||
let item = MockEventTimelineItem(underlyingContent: content)
|
||||
return MessageTimelineItem(item: item, content: content)
|
||||
}
|
||||
|
||||
let underlyingContent: MessageContentProtocol
|
||||
|
||||
// swiftlint:disable:next force_cast
|
||||
func content() -> MatrixRustSDK.TimelineItemContent { underlyingContent as! TimelineItemContent }
|
||||
|
||||
func eventId() -> String? { UUID().uuidString }
|
||||
|
||||
func debugInfo() -> MatrixRustSDK.EventTimelineItemDebugInfo { .init(model: "MockEvent", originalJson: nil, latestEditJson: nil) }
|
||||
|
||||
func isEditable() -> Bool { false }
|
||||
|
||||
func isLocal() -> Bool { false }
|
||||
|
||||
func isOwn() -> Bool { false }
|
||||
|
||||
func isRemote() -> Bool { true }
|
||||
|
||||
func localSendState() -> MatrixRustSDK.EventSendState? { nil }
|
||||
|
||||
func raw() -> String? { nil }
|
||||
|
||||
func reactions() -> [MatrixRustSDK.Reaction] { [] }
|
||||
|
||||
func sender() -> String { "@user:server.com" }
|
||||
|
||||
func senderProfile() -> MatrixRustSDK.ProfileDetails { .unavailable }
|
||||
|
||||
func timestamp() -> UInt64 { 0 }
|
||||
|
||||
func uniqueIdentifier() -> String { eventId() ?? "" }
|
||||
}
|
||||
@@ -155,11 +155,11 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
private extension TextRoomTimelineItem {
|
||||
init(text: String, sender: String, addReactions: Bool = false) {
|
||||
self.init(id: UUID().uuidString,
|
||||
body: text,
|
||||
timestamp: "10:47 am",
|
||||
isOutgoing: sender == "bob",
|
||||
isEditable: sender == "bob",
|
||||
sender: .init(id: "@\(sender):server.com", displayName: sender),
|
||||
content: .init(body: text),
|
||||
properties: RoomTimelineItemProperties(reactions: addReactions ? [
|
||||
AggregatedReaction(key: "🦄", count: 1, isHighlighted: false)
|
||||
] : []))
|
||||
|
||||
Reference in New Issue
Block a user