mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Render Room and Message Pills (#3809)
* added a way to render the room and the message pills, but is WIP * permalinks now get converted into pills! * fixed an issue where room address mentions were not adding a URL properly but a string * updated tests * c * Revert "c" This reverts commit 5c80252fa23dba7e4d44f2a07fbf1e9500e37c82. * updated tests * more tests * created APIs to get a specific RoomSummary given the id or the alias * small mention builder improvement * pr suggestions
This commit is contained in:
@@ -93,7 +93,8 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
appMediator: appMediator,
|
||||
emojiProvider: emojiProvider,
|
||||
userIndicatorController: userIndicatorController,
|
||||
timelineControllerFactory: timelineControllerFactory)
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
clientProxy: userSession.clientProxy)
|
||||
|
||||
let coordinator = MediaEventsTimelineScreenCoordinator(parameters: parameters)
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
||||
appMediator: appMediator,
|
||||
emojiProvider: emojiProvider,
|
||||
timelineControllerFactory: timelineControllerFactory))
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
clientProxy: userSession.clientProxy))
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
|
||||
@@ -3225,6 +3225,146 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
||||
return roomPreviewForIdentifierViaReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - roomSummaryForIdentifier
|
||||
|
||||
var roomSummaryForIdentifierUnderlyingCallsCount = 0
|
||||
var roomSummaryForIdentifierCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return roomSummaryForIdentifierUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = roomSummaryForIdentifierUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
roomSummaryForIdentifierUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
roomSummaryForIdentifierUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var roomSummaryForIdentifierCalled: Bool {
|
||||
return roomSummaryForIdentifierCallsCount > 0
|
||||
}
|
||||
var roomSummaryForIdentifierReceivedIdentifier: String?
|
||||
var roomSummaryForIdentifierReceivedInvocations: [String] = []
|
||||
|
||||
var roomSummaryForIdentifierUnderlyingReturnValue: RoomSummary?
|
||||
var roomSummaryForIdentifierReturnValue: RoomSummary? {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return roomSummaryForIdentifierUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: RoomSummary?? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = roomSummaryForIdentifierUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
roomSummaryForIdentifierUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
roomSummaryForIdentifierUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var roomSummaryForIdentifierClosure: ((String) -> RoomSummary?)?
|
||||
|
||||
func roomSummaryForIdentifier(_ identifier: String) -> RoomSummary? {
|
||||
roomSummaryForIdentifierCallsCount += 1
|
||||
roomSummaryForIdentifierReceivedIdentifier = identifier
|
||||
DispatchQueue.main.async {
|
||||
self.roomSummaryForIdentifierReceivedInvocations.append(identifier)
|
||||
}
|
||||
if let roomSummaryForIdentifierClosure = roomSummaryForIdentifierClosure {
|
||||
return roomSummaryForIdentifierClosure(identifier)
|
||||
} else {
|
||||
return roomSummaryForIdentifierReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - roomSummaryForAlias
|
||||
|
||||
var roomSummaryForAliasUnderlyingCallsCount = 0
|
||||
var roomSummaryForAliasCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return roomSummaryForAliasUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = roomSummaryForAliasUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
roomSummaryForAliasUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
roomSummaryForAliasUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var roomSummaryForAliasCalled: Bool {
|
||||
return roomSummaryForAliasCallsCount > 0
|
||||
}
|
||||
var roomSummaryForAliasReceivedAlias: String?
|
||||
var roomSummaryForAliasReceivedInvocations: [String] = []
|
||||
|
||||
var roomSummaryForAliasUnderlyingReturnValue: RoomSummary?
|
||||
var roomSummaryForAliasReturnValue: RoomSummary? {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return roomSummaryForAliasUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: RoomSummary?? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = roomSummaryForAliasUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
roomSummaryForAliasUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
roomSummaryForAliasUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var roomSummaryForAliasClosure: ((String) -> RoomSummary?)?
|
||||
|
||||
func roomSummaryForAlias(_ alias: String) -> RoomSummary? {
|
||||
roomSummaryForAliasCallsCount += 1
|
||||
roomSummaryForAliasReceivedAlias = alias
|
||||
DispatchQueue.main.async {
|
||||
self.roomSummaryForAliasReceivedInvocations.append(alias)
|
||||
}
|
||||
if let roomSummaryForAliasClosure = roomSummaryForAliasClosure {
|
||||
return roomSummaryForAliasClosure(alias)
|
||||
} else {
|
||||
return roomSummaryForAliasReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - loadUserDisplayName
|
||||
|
||||
var loadUserDisplayNameUnderlyingCallsCount = 0
|
||||
|
||||
@@ -67,6 +67,31 @@ extension RoomSummaryProviderMock {
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomSummary {
|
||||
static func mock(id: String,
|
||||
name: String,
|
||||
canonicalAlias: String? = nil) -> RoomSummary {
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: id,
|
||||
joinRequestType: nil,
|
||||
name: name,
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"),
|
||||
lastMessageFormattedTimestamp: "14:56",
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: .allMessages,
|
||||
canonicalAlias: canonicalAlias,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == RoomSummary {
|
||||
static let mockRooms: [Element] = [
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
@@ -83,6 +108,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: .allMessages,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false),
|
||||
@@ -99,7 +125,8 @@ extension Array where Element == RoomSummary {
|
||||
unreadMentionsCount: 0,
|
||||
unreadNotificationsCount: 2,
|
||||
notificationMode: .mute,
|
||||
canonicalAlias: nil,
|
||||
canonicalAlias: "#foundation-and-empire:matrix.org",
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false),
|
||||
@@ -117,6 +144,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: .mentionsAndKeywordsOnly,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false),
|
||||
@@ -134,6 +162,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadNotificationsCount: 2,
|
||||
notificationMode: .allMessages,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false),
|
||||
@@ -151,6 +180,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadNotificationsCount: 1,
|
||||
notificationMode: .allMessages,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: true,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false),
|
||||
@@ -168,6 +198,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: .mute,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: true,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false),
|
||||
@@ -185,6 +216,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
@@ -235,6 +267,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: "#footest:somewhere.org",
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false),
|
||||
@@ -252,6 +285,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
@@ -224,7 +224,8 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
case .atRoom:
|
||||
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
||||
case .roomAlias(let alias):
|
||||
if let url = try? matrixToRoomAliasPermalink(roomAlias: alias) {
|
||||
if let urlString = try? matrixToRoomAliasPermalink(roomAlias: alias),
|
||||
let url = URL(string: urlString) {
|
||||
attributedString.addAttribute(.link, value: url, range: match.range)
|
||||
}
|
||||
case .matrixURI(let uri):
|
||||
@@ -282,13 +283,13 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
case .user(let userID):
|
||||
mentionBuilder.handleUserMention(for: attributedString, in: range, url: url, userID: userID, userDisplayName: nil)
|
||||
case .room(let roomID):
|
||||
attributedString.addAttributes([.MatrixRoomID: roomID], range: range)
|
||||
mentionBuilder.handleRoomIDMention(for: attributedString, in: range, url: url, roomID: roomID)
|
||||
case .roomAlias(let alias):
|
||||
attributedString.addAttributes([.MatrixRoomAlias: alias], range: range)
|
||||
mentionBuilder.handleRoomAliasMention(for: attributedString, in: range, url: url, roomAlias: alias)
|
||||
case .eventOnRoomId(let roomID, let eventID):
|
||||
attributedString.addAttributes([.MatrixEventOnRoomID: EventOnRoomIDAttribute.Value(roomID: roomID, eventID: eventID)], range: range)
|
||||
mentionBuilder.handleEventOnRoomIDMention(for: attributedString, in: range, url: url, eventID: eventID, roomID: roomID)
|
||||
case .eventOnRoomAlias(let alias, let eventID):
|
||||
attributedString.addAttributes([.MatrixEventOnRoomAlias: EventOnRoomAliasAttribute.Value(alias: alias, eventID: eventID)], range: range)
|
||||
mentionBuilder.handleEventOnRoomAliasMention(for: attributedString, in: range, url: url, eventID: eventID, roomAlias: alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,6 +365,10 @@ extension NSAttributedString.Key {
|
||||
|
||||
protocol MentionBuilderProtocol {
|
||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?)
|
||||
func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String)
|
||||
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String)
|
||||
func handleEventOnRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomAlias: String)
|
||||
func handleEventOnRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomID: String)
|
||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,20 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
struct MentionBuilder: MentionBuilderProtocol {
|
||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?) {
|
||||
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
|
||||
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
|
||||
let blockquote = attributes[.MatrixBlockquote]
|
||||
let foregroundColor = attributes[.foregroundColor] as? UIColor ?? .compound.textPrimary
|
||||
struct AttributesToRestore {
|
||||
let font: UIFont
|
||||
let blockquote: Bool?
|
||||
let foregroundColor: UIColor
|
||||
}
|
||||
|
||||
func handleUserMention(for attributedString: NSMutableAttributedString,
|
||||
in range: NSRange,
|
||||
url: URL,
|
||||
userID: String,
|
||||
userDisplayName: String?) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .user(userID: userID), font: font)
|
||||
let attachmentData = PillTextAttachmentData(type: .user(userID: userID), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixUserID, value: userID, range: range)
|
||||
|
||||
@@ -26,34 +33,151 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID, .font: font, .foregroundColor: foregroundColor]
|
||||
if let blockquote {
|
||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
||||
}
|
||||
let attachmentString = NSMutableAttributedString(attachment: attachment)
|
||||
attachmentString.addAttributes(attachmentAttributes, range: NSRange(location: 0, length: attachmentString.length))
|
||||
attributedString.replaceCharacters(in: range, with: attachmentString)
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixUserID: userID,
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
attachmentAttributes.addMatrixUsernameIfNeeded(userDisplayName)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) {
|
||||
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
|
||||
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
|
||||
let blockquote = attributes[.MatrixBlockquote]
|
||||
let foregroundColor = attributes[.foregroundColor] as? UIColor ?? .compound.textPrimary
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .allUsers, font: font)
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .allUsers, font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font, .MatrixAllUsersMention: true, .foregroundColor: foregroundColor]
|
||||
if let blockquote {
|
||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: attributesToRestore.font,
|
||||
.MatrixAllUsersMention: true,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .roomID(roomID), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixRoomID, value: roomID, range: range)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixRoomID: roomID,
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .roomAlias(roomAlias), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixRoomAlias, value: roomAlias, range: range)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixRoomAlias: roomAlias,
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleEventOnRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomAlias: String) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .event(room: .roomAlias(roomAlias)), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixEventOnRoomAlias, value: EventOnRoomAliasAttribute.Value(alias: roomAlias, eventID: eventID), range: range)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixEventOnRoomAlias: EventOnRoomAliasAttribute.Value(alias: roomAlias, eventID: eventID),
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleEventOnRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomID: String) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .event(room: .roomID(roomID)), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixEventOnRoomID, value: EventOnRoomIDAttribute.Value(roomID: roomID, eventID: eventID), range: range)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixEventOnRoomID: EventOnRoomIDAttribute.Value(roomID: roomID, eventID: eventID),
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
private func getAttributesToRestore(for attributedString: NSMutableAttributedString, in range: NSRange) -> AttributesToRestore {
|
||||
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
|
||||
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
|
||||
let blockquote = attributes[.MatrixBlockquote] as? Bool
|
||||
let foregroundColor = attributes[.foregroundColor] as? UIColor ?? .compound.textPrimary
|
||||
|
||||
return AttributesToRestore(font: font, blockquote: blockquote, foregroundColor: foregroundColor)
|
||||
}
|
||||
|
||||
private func setPillAttachment(attachment: PillTextAttachment,
|
||||
attributedString: NSMutableAttributedString,
|
||||
in range: NSRange,
|
||||
with attributes: [NSAttributedString.Key: Any]) {
|
||||
let attachmentString = NSMutableAttributedString(attachment: attachment)
|
||||
attachmentString.addAttributes(attachmentAttributes, range: NSRange(location: 0, length: attachmentString.length))
|
||||
attachmentString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length))
|
||||
attributedString.replaceCharacters(in: range, with: attachmentString)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Dictionary where Key == NSAttributedString.Key, Value == Any {
|
||||
mutating func addBlockquoteIfNeeded(_ value: Bool?) {
|
||||
if let value {
|
||||
self[.MatrixBlockquote] = value
|
||||
}
|
||||
}
|
||||
|
||||
mutating func addMatrixUsernameIfNeeded(_ value: String?) {
|
||||
if let value {
|
||||
self[.MatrixUserDisplayName] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests {
|
||||
// The mock viewModel simulates the loading logic for testing purposes
|
||||
context = PillContext.mock(type: .loadUser(isOwn: false))
|
||||
context = PillContext.mock(viewState: .mention(isOwnMention: false, displayText: "Alice"), delay: .seconds(2))
|
||||
mediaProvider = MediaProviderMock(configuration: .init())
|
||||
} else if let timelineContext = delegate?.timelineContext {
|
||||
context = PillContext(timelineContext: timelineContext, data: pillData)
|
||||
|
||||
@@ -10,75 +10,77 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
final class PillContext: ObservableObject {
|
||||
struct PillViewState: Equatable {
|
||||
let isOwnMention: Bool
|
||||
let displayText: String
|
||||
}
|
||||
@Published var viewState: PillViewState = .undefined
|
||||
|
||||
@Published private(set) var viewState: PillViewState
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
let data: PillTextAttachmentData
|
||||
var cancellable: AnyCancellable?
|
||||
|
||||
init(timelineContext: TimelineViewModel.Context, data: PillTextAttachmentData) {
|
||||
switch data.type {
|
||||
case let .user(id):
|
||||
let isOwnMention = id == timelineContext.viewState.ownUserID
|
||||
if let profile = timelineContext.viewState.members[id] {
|
||||
var name = id
|
||||
if let displayName = profile.displayName {
|
||||
name = "@\(displayName)"
|
||||
}
|
||||
viewState = PillViewState(isOwnMention: isOwnMention, displayText: name)
|
||||
} else {
|
||||
viewState = PillViewState(isOwnMention: isOwnMention, displayText: id)
|
||||
cancellable = timelineContext.$viewState.sink { [weak self] viewState in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let profile = viewState.members[id] {
|
||||
var name = id
|
||||
if let displayName = profile.displayName {
|
||||
name = "@\(displayName)"
|
||||
}
|
||||
self.viewState = PillViewState(isOwnMention: isOwnMention, displayText: name)
|
||||
cancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
case .allUsers:
|
||||
viewState = PillViewState(isOwnMention: true, displayText: PillConstants.atRoom)
|
||||
}
|
||||
self.data = data
|
||||
timelineContext.viewState.pillContextUpdater?(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension PillContext {
|
||||
enum MockType {
|
||||
case loadUser(isOwn: Bool)
|
||||
case loadedUser(isOwn: Bool)
|
||||
case allUsers
|
||||
static func mock(viewState: PillViewState, delay: Duration? = nil) -> PillContext {
|
||||
// This is just for previews so the internal data doesn't really matter
|
||||
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
|
||||
if let delay {
|
||||
viewModel.viewState = .mention(isOwnMention: false, displayText: "placeholder")
|
||||
Task {
|
||||
try? await Task.sleep(for: delay)
|
||||
viewModel.viewState = viewState
|
||||
}
|
||||
} else {
|
||||
viewModel.viewState = viewState
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
||||
enum PillViewState: Equatable {
|
||||
enum PillImage: Equatable {
|
||||
case link
|
||||
case roomAvatar(RoomAvatar)
|
||||
}
|
||||
|
||||
static func mock(type: MockType) -> PillContext {
|
||||
let testID = "@test:test.com"
|
||||
let pillType: PillType
|
||||
switch type {
|
||||
case .loadUser(let isOwn):
|
||||
pillType = .user(userID: testID)
|
||||
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: testID)
|
||||
Task {
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: "@Test Long Display Text")
|
||||
}
|
||||
return viewModel
|
||||
case .loadedUser(let isOwn):
|
||||
pillType = .user(userID: "@test:test.com")
|
||||
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: "@Very Very Long Test Display Text")
|
||||
return viewModel
|
||||
case .allUsers:
|
||||
pillType = .allUsers
|
||||
return PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
||||
case mention(isOwnMention: Bool, displayText: String)
|
||||
case reference(avatar: PillImage, displayText: String)
|
||||
case undefined
|
||||
|
||||
var isOwnMention: Bool {
|
||||
switch self {
|
||||
case .mention(let isOwnMention, _):
|
||||
return isOwnMention
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var displayText: String {
|
||||
switch self {
|
||||
case .mention(_, let displayText), .reference(_, let displayText):
|
||||
return displayText
|
||||
case .undefined:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var isUndefined: Bool {
|
||||
switch self {
|
||||
case .undefined:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var image: PillImage? {
|
||||
switch self {
|
||||
case .reference(let avatar, _):
|
||||
return avatar
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,21 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
enum PillType: Codable, Equatable {
|
||||
enum EventRoom: Codable, Equatable {
|
||||
case roomAlias(String)
|
||||
case roomID(String)
|
||||
|
||||
var value: String {
|
||||
switch self {
|
||||
case .roomAlias(let value), .roomID(let value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case event(room: EventRoom)
|
||||
case roomAlias(String)
|
||||
case roomID(String)
|
||||
/// A pill that mentions a user
|
||||
case user(userID: String)
|
||||
/// A pill that mentions all users in a room
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct PillView: View {
|
||||
@@ -22,18 +23,42 @@ struct PillView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text(context.viewState.displayText)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundColor(textColor)
|
||||
.lineLimit(1)
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, 6)
|
||||
.padding(.vertical, 1)
|
||||
.background { Capsule().foregroundColor(backgroundColor) }
|
||||
mainContent
|
||||
.onChange(of: context.viewState.displayText) {
|
||||
didChangeText()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
HStack(spacing: 4) {
|
||||
image
|
||||
Text(context.viewState.displayText)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundColor(textColor)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, 6)
|
||||
.padding(.vertical, 1)
|
||||
.background { Capsule().foregroundColor(backgroundColor) }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var image: some View {
|
||||
if let image = context.viewState.image {
|
||||
switch image {
|
||||
case .link:
|
||||
CompoundIcon(\.link, size: .custom(12), relativeTo: .compound.bodyLGSemibold)
|
||||
.padding(2)
|
||||
.foregroundStyle(.compound.bgCanvasDefault)
|
||||
.background(.compound.textLinkExternal)
|
||||
.clipShape(Circle())
|
||||
case .roomAvatar(let avatar):
|
||||
RoomAvatarImage(avatar: avatar, avatarSize: .custom(16), mediaProvider: mediaProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PillView_Previews: PreviewProvider, TestablePreview {
|
||||
@@ -41,24 +66,27 @@ struct PillView_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static var previews: some View {
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .loadUser(isOwn: false))) { }
|
||||
context: PillContext.mock(viewState: .mention(isOwnMention: false,
|
||||
displayText: "@Alice"))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("Loading")
|
||||
.previewDisplayName("User")
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .loadUser(isOwn: true))) { }
|
||||
context: PillContext.mock(viewState: .mention(isOwnMention: false,
|
||||
displayText: "@Alice but with a very very long name"))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("Loading Own")
|
||||
.previewDisplayName("User with a long name")
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .loadedUser(isOwn: false))) { }
|
||||
context: PillContext.mock(viewState: .mention(isOwnMention: true,
|
||||
displayText: "@Alice"))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("Loaded Long")
|
||||
.previewDisplayName("Own user")
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .loadedUser(isOwn: true))) { }
|
||||
context: PillContext.mock(viewState: .reference(avatar: .roomAvatar(.room(id: "roomID", name: "Room", avatarURL: nil)), displayText: "Room"))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("Loaded Long Own")
|
||||
.previewDisplayName("Room")
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .allUsers)) { }
|
||||
context: PillContext.mock(viewState: .reference(avatar: .link, displayText: L10n.screenRoomEventPill("Room")))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("All Users")
|
||||
.previewDisplayName("Message link")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import Foundation
|
||||
|
||||
// In the future we might use this to do some customisation in what is plain text used to represent mentions.
|
||||
struct PlainMentionBuilder: MentionBuilderProtocol {
|
||||
func handleEventOnRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomAlias: String) { }
|
||||
|
||||
func handleEventOnRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomID: String) { }
|
||||
|
||||
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String) { }
|
||||
|
||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) { }
|
||||
|
||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?) {
|
||||
@@ -17,4 +23,6 @@ struct PlainMentionBuilder: MentionBuilderProtocol {
|
||||
}
|
||||
attributedString.insert(NSAttributedString(string: "@"), at: range.location)
|
||||
}
|
||||
|
||||
func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String) { }
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@ private extension HomeScreenRoom {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: "#footest:somewhere.org",
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
@@ -220,6 +221,7 @@ private extension HomeScreenRoom {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: alias,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
@@ -157,6 +157,7 @@ private extension HomeScreenRoom {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: "#footest:somewhere.org",
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
@@ -184,6 +185,7 @@ private extension HomeScreenRoom {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: alias,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
+5
-2
@@ -19,6 +19,7 @@ struct MediaEventsTimelineScreenCoordinatorParameters {
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
let clientProxy: ClientProxyProtocol
|
||||
}
|
||||
|
||||
enum MediaEventsTimelineScreenCoordinatorAction {
|
||||
@@ -49,7 +50,8 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
|
||||
let filesTimelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
||||
timelineController: parameters.filesTimelineController,
|
||||
@@ -61,7 +63,8 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
|
||||
viewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: mediaTimelineViewModel,
|
||||
filesTimelineViewModel: filesTimelineViewModel,
|
||||
|
||||
+2
-1
@@ -274,6 +274,7 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -17,6 +17,7 @@ struct PinnedEventsTimelineScreenCoordinatorParameters {
|
||||
let appMediator: AppMediatorProtocol
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
let clientProxy: ClientProxyProtocol
|
||||
}
|
||||
|
||||
enum PinnedEventsTimelineScreenCoordinatorAction {
|
||||
@@ -53,7 +54,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
+2
-1
@@ -99,7 +99,8 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
@@ -86,7 +86,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
appSettings: parameters.appSettings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
|
||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||
|
||||
@@ -266,7 +266,8 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -41,6 +41,7 @@ class TimelineInteractionHandler {
|
||||
private let emojiProvider: EmojiProviderProtocol
|
||||
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
private let pollInteractionHandler: PollInteractionHandlerProtocol
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<TimelineInteractionHandlerAction, Never> = .init()
|
||||
var actions: AnyPublisher<TimelineInteractionHandlerAction, Never> {
|
||||
@@ -66,7 +67,8 @@ class TimelineInteractionHandler {
|
||||
appSettings: AppSettings,
|
||||
analyticsService: AnalyticsService,
|
||||
emojiProvider: EmojiProviderProtocol,
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol,
|
||||
clientProxy: ClientProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
self.timelineController = timelineController
|
||||
self.mediaProvider = mediaProvider
|
||||
@@ -79,6 +81,7 @@ class TimelineInteractionHandler {
|
||||
self.analyticsService = analyticsService
|
||||
self.emojiProvider = emojiProvider
|
||||
self.timelineControllerFactory = timelineControllerFactory
|
||||
self.clientProxy = clientProxy
|
||||
pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, roomProxy: roomProxy)
|
||||
}
|
||||
|
||||
@@ -580,7 +583,8 @@ class TimelineInteractionHandler {
|
||||
appSettings: appSettings,
|
||||
analyticsService: analyticsService,
|
||||
emojiProvider: emojiProvider,
|
||||
timelineControllerFactory: timelineControllerFactory)
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
clientProxy: clientProxy)
|
||||
|
||||
return .displayMediaPreview(item: item, timelineViewModel: .new(timelineViewModel))
|
||||
} else {
|
||||
|
||||
@@ -114,6 +114,9 @@ struct TimelineViewState: BindableState {
|
||||
/// A closure providing the associated audio player state for an item in the timeline.
|
||||
var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)?
|
||||
|
||||
/// A closure that updates the associated pill context
|
||||
var pillContextUpdater: (@MainActor (PillContext) -> Void)?
|
||||
|
||||
var emojiProvider: EmojiProviderProtocol
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
private let analyticsService: AnalyticsService
|
||||
private let emojiProvider: EmojiProviderProtocol
|
||||
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
|
||||
private let timelineInteractionHandler: TimelineInteractionHandler
|
||||
|
||||
@@ -55,7 +56,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
appSettings: AppSettings,
|
||||
analyticsService: AnalyticsService,
|
||||
emojiProvider: EmojiProviderProtocol,
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol,
|
||||
clientProxy: ClientProxyProtocol) {
|
||||
self.timelineController = timelineController
|
||||
self.mediaProvider = mediaProvider
|
||||
self.mediaPlayerProvider = mediaPlayerProvider
|
||||
@@ -66,6 +68,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
self.appMediator = appMediator
|
||||
self.emojiProvider = emojiProvider
|
||||
self.timelineControllerFactory = timelineControllerFactory
|
||||
self.clientProxy = clientProxy
|
||||
|
||||
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
|
||||
|
||||
@@ -80,7 +83,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
appSettings: appSettings,
|
||||
analyticsService: analyticsService,
|
||||
emojiProvider: emojiProvider,
|
||||
timelineControllerFactory: timelineControllerFactory)
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
clientProxy: clientProxy)
|
||||
|
||||
super.init(initialViewState: TimelineViewState(timelineKind: timelineController.timelineKind,
|
||||
roomID: roomProxy.id,
|
||||
@@ -113,6 +117,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
return self.timelineInteractionHandler.audioPlayerState(for: itemID)
|
||||
}
|
||||
|
||||
state.pillContextUpdater = { [weak self] pillContext in
|
||||
self?.pillContextUpdater(pillContext)
|
||||
}
|
||||
|
||||
state.timelineState.paginationState = timelineController.paginationState
|
||||
buildTimelineViews(timelineItems: timelineController.timelineItems)
|
||||
|
||||
@@ -826,6 +834,72 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
actionsSubject.send(.displayMessageForwarding(forwardingItem: .init(id: itemID, roomID: roomProxy.id, content: content)))
|
||||
}
|
||||
|
||||
// MARK: Pills
|
||||
|
||||
private func pillContextUpdater(_ pillContext: PillContext) {
|
||||
switch pillContext.data.type {
|
||||
case let .user(id):
|
||||
let isOwnMention = id == state.ownUserID
|
||||
if let profile = state.members[id] {
|
||||
var name = id
|
||||
if let displayName = profile.displayName {
|
||||
name = "@\(displayName)"
|
||||
}
|
||||
pillContext.viewState = .mention(isOwnMention: isOwnMention, displayText: name)
|
||||
} else {
|
||||
pillContext.viewState = .mention(isOwnMention: isOwnMention, displayText: id)
|
||||
pillContext.cancellable = context.$viewState
|
||||
.compactMap { $0.members[id] }
|
||||
.sink { [weak pillContext] profile in
|
||||
guard let pillContext else {
|
||||
return
|
||||
}
|
||||
|
||||
var name = id
|
||||
if let displayName = profile.displayName {
|
||||
name = "@\(displayName)"
|
||||
}
|
||||
pillContext.viewState = .mention(isOwnMention: isOwnMention, displayText: name)
|
||||
pillContext.cancellable = nil
|
||||
}
|
||||
}
|
||||
case .allUsers:
|
||||
pillContext.viewState = .mention(isOwnMention: true, displayText: PillConstants.atRoom)
|
||||
case .event(let room):
|
||||
var pillViewState: PillViewState = .reference(avatar: .link, displayText: L10n.screenRoomEventPill(room.value))
|
||||
defer {
|
||||
pillContext.viewState = pillViewState
|
||||
}
|
||||
|
||||
switch room {
|
||||
case .roomAlias(let alias):
|
||||
guard let roomSummary = clientProxy.roomSummaryForAlias(alias) else {
|
||||
return
|
||||
}
|
||||
// We always show the link image for event permalinks
|
||||
pillViewState = .reference(avatar: .link, displayText: L10n.screenRoomEventPill(roomSummary.name))
|
||||
case .roomID(let id):
|
||||
guard let roomSummary = clientProxy.roomSummaryForIdentifier(id) else {
|
||||
return
|
||||
}
|
||||
// We always show the link image for event permalinks
|
||||
pillViewState = .reference(avatar: .link, displayText: L10n.screenRoomEventPill(roomSummary.name))
|
||||
}
|
||||
case .roomAlias(let alias):
|
||||
guard let roomSummary = clientProxy.roomSummaryForAlias(alias) else {
|
||||
pillContext.viewState = .reference(avatar: .link, displayText: alias)
|
||||
return
|
||||
}
|
||||
pillContext.viewState = .reference(avatar: .roomAvatar(roomSummary.avatar), displayText: roomSummary.name)
|
||||
case .roomID(let id):
|
||||
guard let roomSummary = clientProxy.roomSummaryForIdentifier(id) else {
|
||||
pillContext.viewState = .reference(avatar: .link, displayText: id)
|
||||
return
|
||||
}
|
||||
pillContext.viewState = .reference(avatar: .roomAvatar(roomSummary.avatar), displayText: roomSummary.name)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Indicators
|
||||
|
||||
private func showFocusLoadingIndicator() {
|
||||
@@ -896,7 +970,8 @@ extension TimelineViewModel {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
return mock
|
||||
}()
|
||||
|
||||
|
||||
@@ -339,7 +339,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
@@ -88,8 +88,9 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
|
||||
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")]
|
||||
|
||||
@@ -142,8 +142,6 @@ struct FormattedBodyText_Previews: PreviewProvider, TestablePreview {
|
||||
let htmlStrings = [
|
||||
"""
|
||||
Plain text\n
|
||||
@bob:matrix.org\n
|
||||
#room:matrix.org\n
|
||||
!room:matrix.org\n
|
||||
https://www.matrix.org\n
|
||||
www.matrix.org\n
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -91,7 +91,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -515,6 +515,18 @@ class ClientProxy: ClientProxyProtocol {
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func roomSummaryForIdentifier(_ identifier: String) -> RoomSummary? {
|
||||
// the alternate room summary provider is not impacted by filtering
|
||||
alternateRoomSummaryProvider?.roomListPublisher.value.first { $0.id == identifier }
|
||||
}
|
||||
|
||||
func roomSummaryForAlias(_ alias: String) -> RoomSummary? {
|
||||
// the alternate room summary provider is not impacted by filtering
|
||||
alternateRoomSummaryProvider?.roomListPublisher.value.first { roomSummary in
|
||||
roomSummary.canonicalAlias == alias || roomSummary.alternativeAliases.contains(alias)
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserDisplayName() async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
|
||||
@@ -142,6 +142,10 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewProxyProtocol, ClientProxyError>
|
||||
|
||||
func roomSummaryForIdentifier(_ identifier: String) -> RoomSummary?
|
||||
|
||||
func roomSummaryForAlias(_ alias: String) -> RoomSummary?
|
||||
|
||||
@discardableResult func loadUserDisplayName() async -> Result<Void, ClientProxyError>
|
||||
|
||||
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A preview object for the Room. useful to get all the possible info for rooms to which the user is not invited to
|
||||
// sourcery: AutoMockable
|
||||
protocol RoomPreviewProxyProtocol {
|
||||
var info: RoomPreviewInfoProxy { get }
|
||||
|
||||
@@ -18,6 +18,7 @@ enum RoomProxyError: Error {
|
||||
case missingTransactionID
|
||||
}
|
||||
|
||||
/// An enum that describes the relationship between the current user and the room, and contains a reference to the specific implementation of the `RoomProxy`.
|
||||
enum RoomProxyType {
|
||||
case joined(JoinedRoomProxyProtocol)
|
||||
case invited(InvitedRoomProxyProtocol)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
/// A quick summary of a Room, useful to describe and give quick informations for the room list
|
||||
struct RoomSummary {
|
||||
enum JoinRequestType {
|
||||
case invite(inviter: RoomMemberProxyProtocol?)
|
||||
@@ -45,6 +46,7 @@ struct RoomSummary {
|
||||
let unreadNotificationsCount: UInt
|
||||
let notificationMode: RoomNotificationModeProxy?
|
||||
let canonicalAlias: String?
|
||||
let alternativeAliases: Set<String>
|
||||
|
||||
let hasOngoingCall: Bool
|
||||
|
||||
@@ -99,6 +101,7 @@ extension RoomSummary {
|
||||
unreadNotificationsCount = hasUnreadNotifications ? 1 : 0
|
||||
notificationMode = settingsMode
|
||||
canonicalAlias = nil
|
||||
alternativeAliases = []
|
||||
hasOngoingCall = false
|
||||
|
||||
joinRequestType = nil
|
||||
|
||||
@@ -275,6 +275,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
unreadNotificationsCount: UInt(roomInfo.numUnreadNotifications),
|
||||
notificationMode: notificationMode,
|
||||
canonicalAlias: roomInfo.canonicalAlias,
|
||||
alternativeAliases: .init(roomInfo.alternativeAliases),
|
||||
hasOngoingCall: roomInfo.hasRoomCall,
|
||||
isMarkedUnread: roomInfo.isMarkedUnread,
|
||||
isFavourite: roomInfo.isFavourite)
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a53cfbfc88026e695e6fffe2b651a8d68bec0a3fee91875ce2c651d8a4d61de5
|
||||
size 321468
|
||||
oid sha256:0a5300140d0d125b42c74b95e136ca19e809d0ca441f63dcf3e0eff2c38c0522
|
||||
size 319205
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a53cfbfc88026e695e6fffe2b651a8d68bec0a3fee91875ce2c651d8a4d61de5
|
||||
size 321468
|
||||
oid sha256:0a5300140d0d125b42c74b95e136ca19e809d0ca441f63dcf3e0eff2c38c0522
|
||||
size 319205
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e52762fc68301b3b7a95b6702528d2c1ca6fd7ddbe83bbf730162dd23e59d92
|
||||
size 250960
|
||||
oid sha256:974272940990f0fd78e1f403c74ba9714fe8662d79399e91f63307bc35421fdd
|
||||
size 247463
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e52762fc68301b3b7a95b6702528d2c1ca6fd7ddbe83bbf730162dd23e59d92
|
||||
size 250960
|
||||
oid sha256:974272940990f0fd78e1f403c74ba9714fe8662d79399e91f63307bc35421fdd
|
||||
size 247463
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d0a07817576872813714096d839d0d0b17553304b86721312e67a269e82d409
|
||||
size 152891
|
||||
oid sha256:61ebfb0d15c41532a779c91b2ff490a7eb2daaeed9cbc8d9b804535e6d721758
|
||||
size 158570
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:af30ec72f2713ce2776ad0ecf874c9ef2922a50927e715063a13a814b6ef1225
|
||||
size 153269
|
||||
oid sha256:5bf11205a434cc0ff90b275e4475e86134ed15e861f21fb37f96a9fc3b25e262
|
||||
size 158954
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c5fb4b5505c4a01815a357752f4aa43ad1ed86ad29f85bb3f59510c0aadb61e1
|
||||
size 89808
|
||||
oid sha256:b0088c448b0a3e1106a869a747c7dc85c882c3a3553e529eb3e502ccbbb22e25
|
||||
size 94620
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:374174cf42d57f21db62597d20ba29d3a8bde957bf06e1124bf3f27feff33fb7
|
||||
size 90120
|
||||
oid sha256:ba7e2d7a550a145a8f2872986b8eb6d437306390feaa2019329192c16398f642
|
||||
size 94955
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:81be3bd1ac46f06143f2fa4bd73a5168f4dee02e5071f21fc1e96449bef17377
|
||||
size 153595
|
||||
oid sha256:9c92c6e353e7759adf0f75cf9a21297656c5c6daad3cfdbbf39301440b5bf8a0
|
||||
size 159902
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:999e8810095d712fd3e1d9aec7d1c2e83a51fe0c6f805816a0abf65ffae35948
|
||||
size 155531
|
||||
oid sha256:dae65098eaf100bb2b762b5a25b4caa3a366e2780690e9a41a976d0bce707a74
|
||||
size 161798
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c811e030816eeff12ec117631f093c98ea467fe76403d3eb156ef10aa295ac79
|
||||
size 103086
|
||||
oid sha256:bac9d7d51106483c23a118e85a567aafa466ba0a118eae797bd27e8f69d97417
|
||||
size 108070
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5c34f0d2af50da052122137c3220336af0214f08320f669d83baa019606534d7
|
||||
size 102908
|
||||
oid sha256:94508db7f079602a32529ba110fd027d9660c75c1d81f38a2e28a9448f12d5cc
|
||||
size 107929
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ab0efa7ad7ced4f257adf46064a78cba4b07b61e07610b94372aea205803d85d
|
||||
size 72247
|
||||
oid sha256:efe3a163270b708fada60d439d66ae82792fe59ef2a2e6bbaa19f264968e56f1
|
||||
size 72035
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ab0efa7ad7ced4f257adf46064a78cba4b07b61e07610b94372aea205803d85d
|
||||
size 72247
|
||||
oid sha256:efe3a163270b708fada60d439d66ae82792fe59ef2a2e6bbaa19f264968e56f1
|
||||
size 72035
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8df10b2bbfcf7242bff019ca058bbeb0ed948b830e9db0aeb9516d03201431e
|
||||
size 31481
|
||||
oid sha256:b8a1b232998033c6b57f309ae3270216a528a9c6b40243cd00500f867b7d3b38
|
||||
size 31526
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8df10b2bbfcf7242bff019ca058bbeb0ed948b830e9db0aeb9516d03201431e
|
||||
size 31481
|
||||
oid sha256:b8a1b232998033c6b57f309ae3270216a528a9c6b40243cd00500f867b7d3b38
|
||||
size 31526
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4aadf5d83f4c585cf4698b95d1b836c516a27ca89f879fb2a54466f4476f8d0b
|
||||
size 70117
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6788496d70fa922b8b67611210e3834bf83e44f9ee57d7f9dd4cc24d5c9afda2
|
||||
size 73527
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1da3837356af612e584c218fe66756cfbe8e5471b9aa6c0058c37b322d726da5
|
||||
size 73438
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cec492d1d00cd401bf65f4fef497e7ed3265a2c3afbebca7e0ee0ffc63a704ea
|
||||
size 72143
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6e72ed5b84224c7368c31ff8e039026039148471106c72a26a7e907eaa8b9b9
|
||||
size 72074
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d6e55f90134a794fcc1483ff6b0134dfd54285a787622ece157e469ec9019541
|
||||
size 73593
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a3ce3dbcb983f608d38f510dc4ee16e9bc65c9e246aebb3aee8fc46bcaf476e
|
||||
size 69720
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3866839ecf0aab97249e8bb9af2a65e621afab2b249e71794e4146c2f5d920c8
|
||||
size 69868
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fcc37d47cfc3f0b4b9fb715c6d7f01ef9971f822b9c476f131a5dd1a3214b635
|
||||
size 74787
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1b95c2fb2064fcf29b6e618b8b314c574b8868078b600d1b2ca4d8b81a1771c6
|
||||
size 69625
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4aadf5d83f4c585cf4698b95d1b836c516a27ca89f879fb2a54466f4476f8d0b
|
||||
size 70117
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6788496d70fa922b8b67611210e3834bf83e44f9ee57d7f9dd4cc24d5c9afda2
|
||||
size 73527
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1da3837356af612e584c218fe66756cfbe8e5471b9aa6c0058c37b322d726da5
|
||||
size 73438
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cec492d1d00cd401bf65f4fef497e7ed3265a2c3afbebca7e0ee0ffc63a704ea
|
||||
size 72143
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6e72ed5b84224c7368c31ff8e039026039148471106c72a26a7e907eaa8b9b9
|
||||
size 72074
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aded9619b7456485ef3ee0fbfa25d57675ad5e2293cc17430d9cc4a88999f18e
|
||||
size 74763
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a3ce3dbcb983f608d38f510dc4ee16e9bc65c9e246aebb3aee8fc46bcaf476e
|
||||
size 69720
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3866839ecf0aab97249e8bb9af2a65e621afab2b249e71794e4146c2f5d920c8
|
||||
size 69868
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fcc37d47cfc3f0b4b9fb715c6d7f01ef9971f822b9c476f131a5dd1a3214b635
|
||||
size 74787
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1b95c2fb2064fcf29b6e618b8b314c574b8868078b600d1b2ca4d8b81a1771c6
|
||||
size 69625
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83cd25f6384b130d705b3a6f4ef800786a8b4ea81bf301075e0eba25bf2f677c
|
||||
size 29689
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d1e5543ec91df74db7bd467cfbd85a509ba125e4336b921ad51fa85cd618b916
|
||||
size 34006
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62e1b513e728b9f20a2f57cff5fec125327b87104d7bbd2abac93e053826eb82
|
||||
size 33950
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0c580cbb9fe8cef3c10b88fbe9d8cac6a17135a74dacbb1186eba92184115854
|
||||
size 31838
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:006b5e597406f1da118e3a9704346b64c62b47751002f06f69330c844c7582da
|
||||
size 31764
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:59a671e69b145e85e484c45212ac982531bb9f11bddf148a0a1841989d77981a
|
||||
size 33375
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79b26613033be55f6d5bac6ea368aa8dcaf346f7799af773bf1599984ffd6bb3
|
||||
size 30224
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5917c9d41ef154277d1b79ddb3d8a875bdf36c020fd53166a286908e0fdf3fa
|
||||
size 29609
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2f38c8984a2f15bb1785edd0b3d508796a91b668f48503bb31b1f71863b6b9ad
|
||||
size 34390
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5a0d7e0566bfc51fdbeae5f6f6e19120a5d57b333f23c0a96a3d66958ed13330
|
||||
size 30143
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83cd25f6384b130d705b3a6f4ef800786a8b4ea81bf301075e0eba25bf2f677c
|
||||
size 29689
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d1e5543ec91df74db7bd467cfbd85a509ba125e4336b921ad51fa85cd618b916
|
||||
size 34006
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62e1b513e728b9f20a2f57cff5fec125327b87104d7bbd2abac93e053826eb82
|
||||
size 33950
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0c580cbb9fe8cef3c10b88fbe9d8cac6a17135a74dacbb1186eba92184115854
|
||||
size 31838
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:006b5e597406f1da118e3a9704346b64c62b47751002f06f69330c844c7582da
|
||||
size 31764
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d180ee391dc1f9f1f7de6e6f2247f9bfb5d816fa00d8945158a078e452fcac38
|
||||
size 34860
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79b26613033be55f6d5bac6ea368aa8dcaf346f7799af773bf1599984ffd6bb3
|
||||
size 30224
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5917c9d41ef154277d1b79ddb3d8a875bdf36c020fd53166a286908e0fdf3fa
|
||||
size 29609
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2f38c8984a2f15bb1785edd0b3d508796a91b668f48503bb31b1f71863b6b9ad
|
||||
size 34390
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5a0d7e0566bfc51fdbeae5f6f6e19120a5d57b333f23c0a96a3d66958ed13330
|
||||
size 30143
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:81fc617254014ad63c2325d5832a2b758b4d62c4c3e399d2c9b9352fb9d0e0be
|
||||
size 150682
|
||||
oid sha256:29ecf5613971aad7c7890ed563d8b69d3c13ad1b2d9189c6cb3a80af53c6568b
|
||||
size 156966
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29222a03a1d9dab355a9d976e4c116b240e33f1c1dc2d4492ea3a1faa2fefe48
|
||||
size 152165
|
||||
oid sha256:270c52cdad79a7495bded4560a8500f4b9266e0af8d3ec0742a42a2fd7c79888
|
||||
size 158449
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:391e0f9ec34cef26c6d2df7cbdd4e30b1ea71f4c00edc0bbd96550d98430c391
|
||||
size 100964
|
||||
oid sha256:429ace579d36361e16639266d2b14b6a515da57b14a25205c5e2460a1a8fe85e
|
||||
size 105969
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:abd4ee010b07107658b3e3aa4e2947fa3c8d55f4b79466f77461819881c2df8b
|
||||
size 102214
|
||||
oid sha256:f29a6ad8b2d81b1e3ffe1d9d8b526488d7c021bf17b04e74d8d3db38f2955944
|
||||
size 107269
|
||||
|
||||
@@ -425,14 +425,88 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes")
|
||||
}
|
||||
|
||||
func testUserMentionAtachment() {
|
||||
func testUserPermalinkMentionAtachment() {
|
||||
let string = "https://matrix.to/#/@test:matrix.org"
|
||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.link)
|
||||
XCTAssertEqual(attributedStringFromHTML?.userID, "@test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromHTML?.link?.absoluteString, string)
|
||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.link)
|
||||
XCTAssertEqual(attributedStringFromPlain?.userID, "@test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromPlain?.link?.absoluteString, string)
|
||||
}
|
||||
|
||||
func testUserIDMentionAtachment() {
|
||||
let string = "@test:matrix.org"
|
||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.userID, "@test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromHTML?.link?.absoluteString, "https://matrix.to/#/@test:matrix.org")
|
||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
||||
XCTAssertEqual(attributedStringFromPlain?.userID, "@test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromPlain?.link?.absoluteString, "https://matrix.to/#/@test:matrix.org")
|
||||
}
|
||||
|
||||
func testRoomIDPermalinkMentionAttachment() {
|
||||
let string = "https://matrix.to/#/!test:matrix.org"
|
||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.roomID, "!test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromHTML?.link?.absoluteString, string)
|
||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.roomID, "!test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromPlain?.link?.absoluteString, string)
|
||||
}
|
||||
|
||||
func testRoomAliasPermalinkMentionAttachment() {
|
||||
let string = "https://matrix.to/#/#test:matrix.org"
|
||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.roomAlias, "#test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromHTML?.link?.absoluteString, "https://matrix.to/#/%23test:matrix.org")
|
||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.roomAlias, "#test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromPlain?.link?.absoluteString, "https://matrix.to/#/%23test:matrix.org")
|
||||
}
|
||||
|
||||
func testRoomAliasMentionAttachment() {
|
||||
let string = "#test:matrix.org"
|
||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.roomAlias, "#test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromHTML?.link?.absoluteString, "https://matrix.to/#/%23test:matrix.org")
|
||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.roomAlias, "#test:matrix.org")
|
||||
XCTAssertEqual(attributedStringFromPlain?.link?.absoluteString, "https://matrix.to/#/%23test:matrix.org")
|
||||
}
|
||||
|
||||
func testEventRoomIDPermalinkMentionAttachment() {
|
||||
let string = "https://matrix.to/#/!test:matrix.org/$test"
|
||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.eventOnRoomID, .some(.init(roomID: "!test:matrix.org", eventID: "$test")))
|
||||
XCTAssertEqual(attributedStringFromHTML?.link?.absoluteString, string)
|
||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
||||
XCTAssertEqual(attributedStringFromPlain?.eventOnRoomID, .some(.init(roomID: "!test:matrix.org", eventID: "$test")))
|
||||
XCTAssertEqual(attributedStringFromPlain?.link?.absoluteString, string)
|
||||
}
|
||||
|
||||
func testEventRoomAliasPermalinkMentionAttachment() {
|
||||
let string = "https://matrix.to/#/#test:matrix.org/$test"
|
||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||
XCTAssertEqual(attributedStringFromHTML?.eventOnRoomAlias, .some(.init(alias: "#test:matrix.org", eventID: "$test")))
|
||||
XCTAssertEqual(attributedStringFromHTML?.link?.absoluteString, "https://matrix.to/#/%23test:matrix.org/$test")
|
||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
||||
XCTAssertEqual(attributedStringFromPlain?.eventOnRoomAlias, .some(.init(alias: "#test:matrix.org", eventID: "$test")))
|
||||
XCTAssertEqual(attributedStringFromPlain?.link?.absoluteString, "https://matrix.to/#/%23test:matrix.org/$test")
|
||||
}
|
||||
|
||||
func testUserMentionAtachmentInBlockQuotes() {
|
||||
@@ -631,7 +705,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
XCTAssertEqual(foundLink, url)
|
||||
XCTAssertEqual(foundAttachments, 2)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
||||
|
||||
@@ -34,6 +34,7 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
unreadNotificationsCount: unreadNotificationsCount,
|
||||
notificationMode: notificationMode,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: hasOngoingCall,
|
||||
isMarkedUnread: isMarkedUnread,
|
||||
isFavourite: false)
|
||||
|
||||
@@ -92,6 +92,7 @@ class LoggingTests: XCTestCase {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
@@ -12,7 +12,7 @@ import XCTest
|
||||
|
||||
@MainActor
|
||||
class PillContextTests: XCTestCase {
|
||||
func testUser() async throws {
|
||||
func testUser() async {
|
||||
let id = "@test:matrix.org"
|
||||
let proxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||
@@ -27,7 +27,8 @@ class PillContextTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
@@ -39,10 +40,11 @@ class PillContextTests: XCTestCase {
|
||||
await Task.yield()
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertNil(context.viewState.image)
|
||||
XCTAssertEqual(context.viewState.displayText, "@\(name)")
|
||||
}
|
||||
|
||||
func testOwnUser() async throws {
|
||||
func testOwnUser() {
|
||||
let id = "@test:matrix.org"
|
||||
let proxyMock = JoinedRoomProxyMock(.init(name: "Test", ownUserID: id))
|
||||
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||
@@ -57,13 +59,15 @@ class PillContextTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertNil(context.viewState.image)
|
||||
XCTAssertTrue(context.viewState.isOwnMention)
|
||||
}
|
||||
|
||||
func testAllUsers() async throws {
|
||||
func testAllUsers() {
|
||||
let avatarURL = URL(string: "https://matrix.jpg")
|
||||
let id = "test_room"
|
||||
let displayName = "Test"
|
||||
@@ -80,10 +84,216 @@ class PillContextTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertTrue(context.viewState.isOwnMention)
|
||||
XCTAssertNil(context.viewState.image)
|
||||
XCTAssertEqual(context.viewState.displayText, PillConstants.atRoom)
|
||||
}
|
||||
|
||||
func testRoomIDMention() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
let clientMock = ClientProxyMock(.init())
|
||||
clientMock.roomSummaryForIdentifierReturnValue = .mock(id: "1", name: "Foundation 🔭🪐🌌")
|
||||
mockController.roomProxy = proxyMock
|
||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||
timelineController: mockController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: clientMock)
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.image, .roomAvatar(.room(id: "1", name: "Foundation 🔭🪐🌌", avatarURL: nil)))
|
||||
XCTAssertEqual(context.viewState.displayText, "Foundation 🔭🪐🌌")
|
||||
}
|
||||
|
||||
func testRoomIDMentionMissingRoom() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||
timelineController: mockController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.image, .link)
|
||||
XCTAssertEqual(context.viewState.displayText, "1")
|
||||
}
|
||||
|
||||
func testRoomAliasMention() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
let clientMock = ClientProxyMock(.init())
|
||||
clientMock.roomSummaryForAliasReturnValue = .mock(id: "2",
|
||||
name: "Foundation and Empire",
|
||||
canonicalAlias: "#foundation-and-empire:matrix.org")
|
||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||
timelineController: mockController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: clientMock)
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.image, .roomAvatar(.room(id: "2", name: "Foundation and Empire", avatarURL: nil)))
|
||||
XCTAssertEqual(context.viewState.displayText, "Foundation and Empire")
|
||||
}
|
||||
|
||||
func testRoomAliasMentionMissingRoom() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||
timelineController: mockController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.image, .link)
|
||||
XCTAssertEqual(context.viewState.displayText, "#foundation-and-empire:matrix.org")
|
||||
}
|
||||
|
||||
func testEventOnRoomIDMention() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
let clientMock = ClientProxyMock(.init())
|
||||
clientMock.roomSummaryForIdentifierReturnValue = .mock(id: "1", name: "Foundation 🔭🪐🌌")
|
||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||
timelineController: mockController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: clientMock)
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.image, .link)
|
||||
XCTAssertEqual(context.viewState.displayText, L10n.screenRoomEventPill("Foundation 🔭🪐🌌"))
|
||||
}
|
||||
|
||||
func testEventOnRoomIDMentionMissingRoom() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||
timelineController: mockController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.image, .link)
|
||||
XCTAssertEqual(context.viewState.displayText, L10n.screenRoomEventPill("1"))
|
||||
}
|
||||
|
||||
func testEventOnRoomAliasMention() async throws {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
let clientMock = ClientProxyMock(.init())
|
||||
clientMock.roomSummaryForAliasReturnValue = .mock(id: "2",
|
||||
name: "Foundation and Empire",
|
||||
canonicalAlias: "#foundation-and-empire:matrix.org")
|
||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||
timelineController: mockController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: clientMock)
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.image, .link)
|
||||
XCTAssertEqual(context.viewState.displayText, L10n.screenRoomEventPill("Foundation and Empire"))
|
||||
}
|
||||
|
||||
func testEventOnRoomAliasMentionMissingRoom() async throws {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
let mock = TimelineViewModel(roomProxy: proxyMock,
|
||||
timelineController: mockController,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: AppMediatorMock.default,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.image, .link)
|
||||
XCTAssertEqual(context.viewState.displayText, L10n.screenRoomEventPill("#foundation-and-empire:matrix.org"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class RoomSummaryTests: XCTestCase {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: nil,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
@@ -312,7 +312,8 @@ class TimelineViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
return (viewModel, roomProxy, timelineProxy, timelineController)
|
||||
}
|
||||
|
||||
@@ -338,7 +339,8 @@ class TimelineViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
||||
value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts
|
||||
@@ -367,7 +369,8 @@ class TimelineViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
XCTAssertEqual(configuration.pinnedEventIDs, viewModel.context.viewState.pinnedEventIDs)
|
||||
|
||||
configuration.pinnedEventIDs = ["test1", "test2"]
|
||||
@@ -394,7 +397,8 @@ class TimelineViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
var deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
||||
value.canCurrentUserPin
|
||||
@@ -425,7 +429,8 @@ class TimelineViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user