mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +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,
|
appMediator: appMediator,
|
||||||
emojiProvider: emojiProvider,
|
emojiProvider: emojiProvider,
|
||||||
userIndicatorController: userIndicatorController,
|
userIndicatorController: userIndicatorController,
|
||||||
timelineControllerFactory: timelineControllerFactory)
|
timelineControllerFactory: timelineControllerFactory,
|
||||||
|
clientProxy: userSession.clientProxy)
|
||||||
|
|
||||||
let coordinator = MediaEventsTimelineScreenCoordinator(parameters: parameters)
|
let coordinator = MediaEventsTimelineScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
||||||
appMediator: appMediator,
|
appMediator: appMediator,
|
||||||
emojiProvider: emojiProvider,
|
emojiProvider: emojiProvider,
|
||||||
timelineControllerFactory: timelineControllerFactory))
|
timelineControllerFactory: timelineControllerFactory,
|
||||||
|
clientProxy: userSession.clientProxy))
|
||||||
|
|
||||||
coordinator.actions
|
coordinator.actions
|
||||||
.sink { [weak self] action in
|
.sink { [weak self] action in
|
||||||
|
|||||||
@@ -3225,6 +3225,146 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
|||||||
return roomPreviewForIdentifierViaReturnValue
|
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
|
//MARK: - loadUserDisplayName
|
||||||
|
|
||||||
var loadUserDisplayNameUnderlyingCallsCount = 0
|
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 {
|
extension Array where Element == RoomSummary {
|
||||||
static let mockRooms: [Element] = [
|
static let mockRooms: [Element] = [
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
@@ -83,6 +108,7 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: .allMessages,
|
notificationMode: .allMessages,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
@@ -99,7 +125,8 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadMentionsCount: 0,
|
unreadMentionsCount: 0,
|
||||||
unreadNotificationsCount: 2,
|
unreadNotificationsCount: 2,
|
||||||
notificationMode: .mute,
|
notificationMode: .mute,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: "#foundation-and-empire:matrix.org",
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
@@ -117,6 +144,7 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: .mentionsAndKeywordsOnly,
|
notificationMode: .mentionsAndKeywordsOnly,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
@@ -134,6 +162,7 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadNotificationsCount: 2,
|
unreadNotificationsCount: 2,
|
||||||
notificationMode: .allMessages,
|
notificationMode: .allMessages,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
@@ -151,6 +180,7 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadNotificationsCount: 1,
|
unreadNotificationsCount: 1,
|
||||||
notificationMode: .allMessages,
|
notificationMode: .allMessages,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: true,
|
hasOngoingCall: true,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
@@ -168,6 +198,7 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: .mute,
|
notificationMode: .mute,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: true,
|
hasOngoingCall: true,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
@@ -185,6 +216,7 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
@@ -235,6 +267,7 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: "#footest:somewhere.org",
|
canonicalAlias: "#footest:somewhere.org",
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
@@ -252,6 +285,7 @@ extension Array where Element == RoomSummary {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
case .atRoom:
|
case .atRoom:
|
||||||
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
||||||
case .roomAlias(let alias):
|
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)
|
attributedString.addAttribute(.link, value: url, range: match.range)
|
||||||
}
|
}
|
||||||
case .matrixURI(let uri):
|
case .matrixURI(let uri):
|
||||||
@@ -282,13 +283,13 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
case .user(let userID):
|
case .user(let userID):
|
||||||
mentionBuilder.handleUserMention(for: attributedString, in: range, url: url, userID: userID, userDisplayName: nil)
|
mentionBuilder.handleUserMention(for: attributedString, in: range, url: url, userID: userID, userDisplayName: nil)
|
||||||
case .room(let roomID):
|
case .room(let roomID):
|
||||||
attributedString.addAttributes([.MatrixRoomID: roomID], range: range)
|
mentionBuilder.handleRoomIDMention(for: attributedString, in: range, url: url, roomID: roomID)
|
||||||
case .roomAlias(let alias):
|
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):
|
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):
|
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 {
|
protocol MentionBuilderProtocol {
|
||||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?)
|
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)
|
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,20 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct MentionBuilder: MentionBuilderProtocol {
|
struct MentionBuilder: MentionBuilderProtocol {
|
||||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?) {
|
struct AttributesToRestore {
|
||||||
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
|
let font: UIFont
|
||||||
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
|
let blockquote: Bool?
|
||||||
let blockquote = attributes[.MatrixBlockquote]
|
let foregroundColor: UIColor
|
||||||
let foregroundColor = attributes[.foregroundColor] as? UIColor ?? .compound.textPrimary
|
}
|
||||||
|
|
||||||
|
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 {
|
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||||
attributedString.addAttribute(.MatrixUserID, value: userID, range: range)
|
attributedString.addAttribute(.MatrixUserID, value: userID, range: range)
|
||||||
|
|
||||||
@@ -26,34 +33,151 @@ struct MentionBuilder: MentionBuilderProtocol {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID, .font: font, .foregroundColor: foregroundColor]
|
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||||
if let blockquote {
|
.MatrixUserID: userID,
|
||||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
.font: attributesToRestore.font,
|
||||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
.foregroundColor: attributesToRestore.foregroundColor]
|
||||||
}
|
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||||
let attachmentString = NSMutableAttributedString(attachment: attachment)
|
attachmentAttributes.addMatrixUsernameIfNeeded(userDisplayName)
|
||||||
attachmentString.addAttributes(attachmentAttributes, range: NSRange(location: 0, length: attachmentString.length))
|
|
||||||
attributedString.replaceCharacters(in: range, with: attachmentString)
|
setPillAttachment(attachment: attachment,
|
||||||
|
attributedString: attributedString,
|
||||||
|
in: range,
|
||||||
|
with: attachmentAttributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) {
|
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) {
|
||||||
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
|
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||||
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
|
|
||||||
let blockquote = attributes[.MatrixBlockquote]
|
let attachmentData = PillTextAttachmentData(type: .allUsers, font: attributesToRestore.font)
|
||||||
let foregroundColor = attributes[.foregroundColor] as? UIColor ?? .compound.textPrimary
|
|
||||||
|
|
||||||
let attachmentData = PillTextAttachmentData(type: .allUsers, font: font)
|
|
||||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font, .MatrixAllUsersMention: true, .foregroundColor: foregroundColor]
|
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: attributesToRestore.font,
|
||||||
if let blockquote {
|
.MatrixAllUsersMention: true,
|
||||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
.foregroundColor: attributesToRestore.foregroundColor]
|
||||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
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)
|
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)
|
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?
|
let mediaProvider: MediaProviderProtocol?
|
||||||
if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests {
|
if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests {
|
||||||
// The mock viewModel simulates the loading logic for testing purposes
|
// 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())
|
mediaProvider = MediaProviderMock(configuration: .init())
|
||||||
} else if let timelineContext = delegate?.timelineContext {
|
} else if let timelineContext = delegate?.timelineContext {
|
||||||
context = PillContext(timelineContext: timelineContext, data: pillData)
|
context = PillContext(timelineContext: timelineContext, data: pillData)
|
||||||
|
|||||||
@@ -10,75 +10,77 @@ import Foundation
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class PillContext: ObservableObject {
|
final class PillContext: ObservableObject {
|
||||||
struct PillViewState: Equatable {
|
@Published var viewState: PillViewState = .undefined
|
||||||
let isOwnMention: Bool
|
|
||||||
let displayText: String
|
|
||||||
}
|
|
||||||
|
|
||||||
@Published private(set) var viewState: PillViewState
|
let data: PillTextAttachmentData
|
||||||
|
var cancellable: AnyCancellable?
|
||||||
private var cancellable: AnyCancellable?
|
|
||||||
|
|
||||||
init(timelineContext: TimelineViewModel.Context, data: PillTextAttachmentData) {
|
init(timelineContext: TimelineViewModel.Context, data: PillTextAttachmentData) {
|
||||||
switch data.type {
|
self.data = data
|
||||||
case let .user(id):
|
timelineContext.viewState.pillContextUpdater?(self)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PillContext {
|
extension PillContext {
|
||||||
enum MockType {
|
static func mock(viewState: PillViewState, delay: Duration? = nil) -> PillContext {
|
||||||
case loadUser(isOwn: Bool)
|
// This is just for previews so the internal data doesn't really matter
|
||||||
case loadedUser(isOwn: Bool)
|
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
|
||||||
case allUsers
|
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 {
|
case mention(isOwnMention: Bool, displayText: String)
|
||||||
let testID = "@test:test.com"
|
case reference(avatar: PillImage, displayText: String)
|
||||||
let pillType: PillType
|
case undefined
|
||||||
switch type {
|
|
||||||
case .loadUser(let isOwn):
|
var isOwnMention: Bool {
|
||||||
pillType = .user(userID: testID)
|
switch self {
|
||||||
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
case .mention(let isOwnMention, _):
|
||||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: testID)
|
return isOwnMention
|
||||||
Task {
|
default:
|
||||||
try? await Task.sleep(for: .seconds(2))
|
return false
|
||||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: "@Test Long Display Text")
|
}
|
||||||
}
|
}
|
||||||
return viewModel
|
|
||||||
case .loadedUser(let isOwn):
|
var displayText: String {
|
||||||
pillType = .user(userID: "@test:test.com")
|
switch self {
|
||||||
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
case .mention(_, let displayText), .reference(_, let displayText):
|
||||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: "@Very Very Long Test Display Text")
|
return displayText
|
||||||
return viewModel
|
case .undefined:
|
||||||
case .allUsers:
|
return ""
|
||||||
pillType = .allUsers
|
}
|
||||||
return PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
}
|
||||||
|
|
||||||
|
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
|
import UIKit
|
||||||
|
|
||||||
enum PillType: Codable, Equatable {
|
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
|
/// A pill that mentions a user
|
||||||
case user(userID: String)
|
case user(userID: String)
|
||||||
/// A pill that mentions all users in a room
|
/// A pill that mentions all users in a room
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// Please see LICENSE files in the repository root for full details.
|
// Please see LICENSE files in the repository root for full details.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Compound
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PillView: View {
|
struct PillView: View {
|
||||||
@@ -22,18 +23,42 @@ struct PillView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(context.viewState.displayText)
|
mainContent
|
||||||
.font(.compound.bodyLGSemibold)
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
.lineLimit(1)
|
|
||||||
.padding(.leading, 4)
|
|
||||||
.padding(.trailing, 6)
|
|
||||||
.padding(.vertical, 1)
|
|
||||||
.background { Capsule().foregroundColor(backgroundColor) }
|
|
||||||
.onChange(of: context.viewState.displayText) {
|
.onChange(of: context.viewState.displayText) {
|
||||||
didChangeText()
|
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 {
|
struct PillView_Previews: PreviewProvider, TestablePreview {
|
||||||
@@ -41,24 +66,27 @@ struct PillView_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
PillView(mediaProvider: mockMediaProvider,
|
PillView(mediaProvider: mockMediaProvider,
|
||||||
context: PillContext.mock(type: .loadUser(isOwn: false))) { }
|
context: PillContext.mock(viewState: .mention(isOwnMention: false,
|
||||||
|
displayText: "@Alice"))) { }
|
||||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||||
.previewDisplayName("Loading")
|
.previewDisplayName("User")
|
||||||
PillView(mediaProvider: mockMediaProvider,
|
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)
|
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||||
.previewDisplayName("Loading Own")
|
.previewDisplayName("User with a long name")
|
||||||
PillView(mediaProvider: mockMediaProvider,
|
PillView(mediaProvider: mockMediaProvider,
|
||||||
context: PillContext.mock(type: .loadedUser(isOwn: false))) { }
|
context: PillContext.mock(viewState: .mention(isOwnMention: true,
|
||||||
|
displayText: "@Alice"))) { }
|
||||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||||
.previewDisplayName("Loaded Long")
|
.previewDisplayName("Own user")
|
||||||
PillView(mediaProvider: mockMediaProvider,
|
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)
|
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||||
.previewDisplayName("Loaded Long Own")
|
.previewDisplayName("Room")
|
||||||
PillView(mediaProvider: mockMediaProvider,
|
PillView(mediaProvider: mockMediaProvider,
|
||||||
context: PillContext.mock(type: .allUsers)) { }
|
context: PillContext.mock(viewState: .reference(avatar: .link, displayText: L10n.screenRoomEventPill("Room")))) { }
|
||||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
.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.
|
// In the future we might use this to do some customisation in what is plain text used to represent mentions.
|
||||||
struct PlainMentionBuilder: MentionBuilderProtocol {
|
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 handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) { }
|
||||||
|
|
||||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?) {
|
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)
|
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,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: "#footest:somewhere.org",
|
canonicalAlias: "#footest:somewhere.org",
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
@@ -220,6 +221,7 @@ private extension HomeScreenRoom {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: alias,
|
canonicalAlias: alias,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ private extension HomeScreenRoom {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: "#footest:somewhere.org",
|
canonicalAlias: "#footest:somewhere.org",
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
@@ -184,6 +185,7 @@ private extension HomeScreenRoom {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: alias,
|
canonicalAlias: alias,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
|
|||||||
+5
-2
@@ -19,6 +19,7 @@ struct MediaEventsTimelineScreenCoordinatorParameters {
|
|||||||
let emojiProvider: EmojiProviderProtocol
|
let emojiProvider: EmojiProviderProtocol
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||||
|
let clientProxy: ClientProxyProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MediaEventsTimelineScreenCoordinatorAction {
|
enum MediaEventsTimelineScreenCoordinatorAction {
|
||||||
@@ -49,7 +50,8 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: parameters.emojiProvider,
|
emojiProvider: parameters.emojiProvider,
|
||||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||||
|
clientProxy: parameters.clientProxy)
|
||||||
|
|
||||||
let filesTimelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
let filesTimelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
||||||
timelineController: parameters.filesTimelineController,
|
timelineController: parameters.filesTimelineController,
|
||||||
@@ -61,7 +63,8 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: parameters.emojiProvider,
|
emojiProvider: parameters.emojiProvider,
|
||||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||||
|
clientProxy: parameters.clientProxy)
|
||||||
|
|
||||||
viewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: mediaTimelineViewModel,
|
viewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: mediaTimelineViewModel,
|
||||||
filesTimelineViewModel: filesTimelineViewModel,
|
filesTimelineViewModel: filesTimelineViewModel,
|
||||||
|
|||||||
+2
-1
@@ -274,6 +274,7 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
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 appMediator: AppMediatorProtocol
|
||||||
let emojiProvider: EmojiProviderProtocol
|
let emojiProvider: EmojiProviderProtocol
|
||||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||||
|
let clientProxy: ClientProxyProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PinnedEventsTimelineScreenCoordinatorAction {
|
enum PinnedEventsTimelineScreenCoordinatorAction {
|
||||||
@@ -53,7 +54,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: parameters.emojiProvider,
|
emojiProvider: parameters.emojiProvider,
|
||||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||||
|
clientProxy: parameters.clientProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
|
|||||||
+2
-1
@@ -99,7 +99,8 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
appSettings: parameters.appSettings,
|
appSettings: parameters.appSettings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: parameters.emojiProvider,
|
emojiProvider: parameters.emojiProvider,
|
||||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||||
|
clientProxy: parameters.clientProxy)
|
||||||
|
|
||||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||||
|
|||||||
@@ -266,7 +266,8 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class TimelineInteractionHandler {
|
|||||||
private let emojiProvider: EmojiProviderProtocol
|
private let emojiProvider: EmojiProviderProtocol
|
||||||
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||||
private let pollInteractionHandler: PollInteractionHandlerProtocol
|
private let pollInteractionHandler: PollInteractionHandlerProtocol
|
||||||
|
private let clientProxy: ClientProxyProtocol
|
||||||
|
|
||||||
private let actionsSubject: PassthroughSubject<TimelineInteractionHandlerAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<TimelineInteractionHandlerAction, Never> = .init()
|
||||||
var actions: AnyPublisher<TimelineInteractionHandlerAction, Never> {
|
var actions: AnyPublisher<TimelineInteractionHandlerAction, Never> {
|
||||||
@@ -66,7 +67,8 @@ class TimelineInteractionHandler {
|
|||||||
appSettings: AppSettings,
|
appSettings: AppSettings,
|
||||||
analyticsService: AnalyticsService,
|
analyticsService: AnalyticsService,
|
||||||
emojiProvider: EmojiProviderProtocol,
|
emojiProvider: EmojiProviderProtocol,
|
||||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
timelineControllerFactory: TimelineControllerFactoryProtocol,
|
||||||
|
clientProxy: ClientProxyProtocol) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
self.timelineController = timelineController
|
self.timelineController = timelineController
|
||||||
self.mediaProvider = mediaProvider
|
self.mediaProvider = mediaProvider
|
||||||
@@ -79,6 +81,7 @@ class TimelineInteractionHandler {
|
|||||||
self.analyticsService = analyticsService
|
self.analyticsService = analyticsService
|
||||||
self.emojiProvider = emojiProvider
|
self.emojiProvider = emojiProvider
|
||||||
self.timelineControllerFactory = timelineControllerFactory
|
self.timelineControllerFactory = timelineControllerFactory
|
||||||
|
self.clientProxy = clientProxy
|
||||||
pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, roomProxy: roomProxy)
|
pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, roomProxy: roomProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +583,8 @@ class TimelineInteractionHandler {
|
|||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
analyticsService: analyticsService,
|
analyticsService: analyticsService,
|
||||||
emojiProvider: emojiProvider,
|
emojiProvider: emojiProvider,
|
||||||
timelineControllerFactory: timelineControllerFactory)
|
timelineControllerFactory: timelineControllerFactory,
|
||||||
|
clientProxy: clientProxy)
|
||||||
|
|
||||||
return .displayMediaPreview(item: item, timelineViewModel: .new(timelineViewModel))
|
return .displayMediaPreview(item: item, timelineViewModel: .new(timelineViewModel))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ struct TimelineViewState: BindableState {
|
|||||||
/// A closure providing the associated audio player state for an item in the timeline.
|
/// A closure providing the associated audio player state for an item in the timeline.
|
||||||
var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)?
|
var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)?
|
||||||
|
|
||||||
|
/// A closure that updates the associated pill context
|
||||||
|
var pillContextUpdater: (@MainActor (PillContext) -> Void)?
|
||||||
|
|
||||||
var emojiProvider: EmojiProviderProtocol
|
var emojiProvider: EmojiProviderProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
private let analyticsService: AnalyticsService
|
private let analyticsService: AnalyticsService
|
||||||
private let emojiProvider: EmojiProviderProtocol
|
private let emojiProvider: EmojiProviderProtocol
|
||||||
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||||
|
private let clientProxy: ClientProxyProtocol
|
||||||
|
|
||||||
private let timelineInteractionHandler: TimelineInteractionHandler
|
private let timelineInteractionHandler: TimelineInteractionHandler
|
||||||
|
|
||||||
@@ -55,7 +56,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
appSettings: AppSettings,
|
appSettings: AppSettings,
|
||||||
analyticsService: AnalyticsService,
|
analyticsService: AnalyticsService,
|
||||||
emojiProvider: EmojiProviderProtocol,
|
emojiProvider: EmojiProviderProtocol,
|
||||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
timelineControllerFactory: TimelineControllerFactoryProtocol,
|
||||||
|
clientProxy: ClientProxyProtocol) {
|
||||||
self.timelineController = timelineController
|
self.timelineController = timelineController
|
||||||
self.mediaProvider = mediaProvider
|
self.mediaProvider = mediaProvider
|
||||||
self.mediaPlayerProvider = mediaPlayerProvider
|
self.mediaPlayerProvider = mediaPlayerProvider
|
||||||
@@ -66,6 +68,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
self.appMediator = appMediator
|
self.appMediator = appMediator
|
||||||
self.emojiProvider = emojiProvider
|
self.emojiProvider = emojiProvider
|
||||||
self.timelineControllerFactory = timelineControllerFactory
|
self.timelineControllerFactory = timelineControllerFactory
|
||||||
|
self.clientProxy = clientProxy
|
||||||
|
|
||||||
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
|
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
|
||||||
|
|
||||||
@@ -80,7 +83,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
analyticsService: analyticsService,
|
analyticsService: analyticsService,
|
||||||
emojiProvider: emojiProvider,
|
emojiProvider: emojiProvider,
|
||||||
timelineControllerFactory: timelineControllerFactory)
|
timelineControllerFactory: timelineControllerFactory,
|
||||||
|
clientProxy: clientProxy)
|
||||||
|
|
||||||
super.init(initialViewState: TimelineViewState(timelineKind: timelineController.timelineKind,
|
super.init(initialViewState: TimelineViewState(timelineKind: timelineController.timelineKind,
|
||||||
roomID: roomProxy.id,
|
roomID: roomProxy.id,
|
||||||
@@ -113,6 +117,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
return self.timelineInteractionHandler.audioPlayerState(for: itemID)
|
return self.timelineInteractionHandler.audioPlayerState(for: itemID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.pillContextUpdater = { [weak self] pillContext in
|
||||||
|
self?.pillContextUpdater(pillContext)
|
||||||
|
}
|
||||||
|
|
||||||
state.timelineState.paginationState = timelineController.paginationState
|
state.timelineState.paginationState = timelineController.paginationState
|
||||||
buildTimelineViews(timelineItems: timelineController.timelineItems)
|
buildTimelineViews(timelineItems: timelineController.timelineItems)
|
||||||
|
|
||||||
@@ -826,6 +834,72 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
actionsSubject.send(.displayMessageForwarding(forwardingItem: .init(id: itemID, roomID: roomProxy.id, content: content)))
|
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
|
// MARK: - User Indicators
|
||||||
|
|
||||||
private func showFocusLoadingIndicator() {
|
private func showFocusLoadingIndicator() {
|
||||||
@@ -896,7 +970,8 @@ extension TimelineViewModel {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
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,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
return mock
|
return mock
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -339,7 +339,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|||||||
@@ -88,8 +88,9 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
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 singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
|
||||||
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
|
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
|
||||||
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")]
|
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")]
|
||||||
|
|||||||
@@ -142,8 +142,6 @@ struct FormattedBodyText_Previews: PreviewProvider, TestablePreview {
|
|||||||
let htmlStrings = [
|
let htmlStrings = [
|
||||||
"""
|
"""
|
||||||
Plain text\n
|
Plain text\n
|
||||||
@bob:matrix.org\n
|
|
||||||
#room:matrix.org\n
|
|
||||||
!room:matrix.org\n
|
!room:matrix.org\n
|
||||||
https://www.matrix.org\n
|
https://www.matrix.org\n
|
||||||
www.matrix.org\n
|
www.matrix.org\n
|
||||||
|
|||||||
+2
-1
@@ -98,7 +98,8 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
|||||||
@@ -515,6 +515,18 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
return .failure(.sdkError(error))
|
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> {
|
func loadUserDisplayName() async -> Result<Void, ClientProxyError> {
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -142,6 +142,10 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
|
|
||||||
func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewProxyProtocol, ClientProxyError>
|
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>
|
@discardableResult func loadUserDisplayName() async -> Result<Void, ClientProxyError>
|
||||||
|
|
||||||
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError>
|
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
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
|
// sourcery: AutoMockable
|
||||||
protocol RoomPreviewProxyProtocol {
|
protocol RoomPreviewProxyProtocol {
|
||||||
var info: RoomPreviewInfoProxy { get }
|
var info: RoomPreviewInfoProxy { get }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ enum RoomProxyError: Error {
|
|||||||
case missingTransactionID
|
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 {
|
enum RoomProxyType {
|
||||||
case joined(JoinedRoomProxyProtocol)
|
case joined(JoinedRoomProxyProtocol)
|
||||||
case invited(InvitedRoomProxyProtocol)
|
case invited(InvitedRoomProxyProtocol)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
|
/// A quick summary of a Room, useful to describe and give quick informations for the room list
|
||||||
struct RoomSummary {
|
struct RoomSummary {
|
||||||
enum JoinRequestType {
|
enum JoinRequestType {
|
||||||
case invite(inviter: RoomMemberProxyProtocol?)
|
case invite(inviter: RoomMemberProxyProtocol?)
|
||||||
@@ -45,6 +46,7 @@ struct RoomSummary {
|
|||||||
let unreadNotificationsCount: UInt
|
let unreadNotificationsCount: UInt
|
||||||
let notificationMode: RoomNotificationModeProxy?
|
let notificationMode: RoomNotificationModeProxy?
|
||||||
let canonicalAlias: String?
|
let canonicalAlias: String?
|
||||||
|
let alternativeAliases: Set<String>
|
||||||
|
|
||||||
let hasOngoingCall: Bool
|
let hasOngoingCall: Bool
|
||||||
|
|
||||||
@@ -99,6 +101,7 @@ extension RoomSummary {
|
|||||||
unreadNotificationsCount = hasUnreadNotifications ? 1 : 0
|
unreadNotificationsCount = hasUnreadNotifications ? 1 : 0
|
||||||
notificationMode = settingsMode
|
notificationMode = settingsMode
|
||||||
canonicalAlias = nil
|
canonicalAlias = nil
|
||||||
|
alternativeAliases = []
|
||||||
hasOngoingCall = false
|
hasOngoingCall = false
|
||||||
|
|
||||||
joinRequestType = nil
|
joinRequestType = nil
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
unreadNotificationsCount: UInt(roomInfo.numUnreadNotifications),
|
unreadNotificationsCount: UInt(roomInfo.numUnreadNotifications),
|
||||||
notificationMode: notificationMode,
|
notificationMode: notificationMode,
|
||||||
canonicalAlias: roomInfo.canonicalAlias,
|
canonicalAlias: roomInfo.canonicalAlias,
|
||||||
|
alternativeAliases: .init(roomInfo.alternativeAliases),
|
||||||
hasOngoingCall: roomInfo.hasRoomCall,
|
hasOngoingCall: roomInfo.hasRoomCall,
|
||||||
isMarkedUnread: roomInfo.isMarkedUnread,
|
isMarkedUnread: roomInfo.isMarkedUnread,
|
||||||
isFavourite: roomInfo.isFavourite)
|
isFavourite: roomInfo.isFavourite)
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a53cfbfc88026e695e6fffe2b651a8d68bec0a3fee91875ce2c651d8a4d61de5
|
oid sha256:0a5300140d0d125b42c74b95e136ca19e809d0ca441f63dcf3e0eff2c38c0522
|
||||||
size 321468
|
size 319205
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a53cfbfc88026e695e6fffe2b651a8d68bec0a3fee91875ce2c651d8a4d61de5
|
oid sha256:0a5300140d0d125b42c74b95e136ca19e809d0ca441f63dcf3e0eff2c38c0522
|
||||||
size 321468
|
size 319205
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0e52762fc68301b3b7a95b6702528d2c1ca6fd7ddbe83bbf730162dd23e59d92
|
oid sha256:974272940990f0fd78e1f403c74ba9714fe8662d79399e91f63307bc35421fdd
|
||||||
size 250960
|
size 247463
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0e52762fc68301b3b7a95b6702528d2c1ca6fd7ddbe83bbf730162dd23e59d92
|
oid sha256:974272940990f0fd78e1f403c74ba9714fe8662d79399e91f63307bc35421fdd
|
||||||
size 250960
|
size 247463
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9d0a07817576872813714096d839d0d0b17553304b86721312e67a269e82d409
|
oid sha256:61ebfb0d15c41532a779c91b2ff490a7eb2daaeed9cbc8d9b804535e6d721758
|
||||||
size 152891
|
size 158570
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:af30ec72f2713ce2776ad0ecf874c9ef2922a50927e715063a13a814b6ef1225
|
oid sha256:5bf11205a434cc0ff90b275e4475e86134ed15e861f21fb37f96a9fc3b25e262
|
||||||
size 153269
|
size 158954
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c5fb4b5505c4a01815a357752f4aa43ad1ed86ad29f85bb3f59510c0aadb61e1
|
oid sha256:b0088c448b0a3e1106a869a747c7dc85c882c3a3553e529eb3e502ccbbb22e25
|
||||||
size 89808
|
size 94620
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:374174cf42d57f21db62597d20ba29d3a8bde957bf06e1124bf3f27feff33fb7
|
oid sha256:ba7e2d7a550a145a8f2872986b8eb6d437306390feaa2019329192c16398f642
|
||||||
size 90120
|
size 94955
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:81be3bd1ac46f06143f2fa4bd73a5168f4dee02e5071f21fc1e96449bef17377
|
oid sha256:9c92c6e353e7759adf0f75cf9a21297656c5c6daad3cfdbbf39301440b5bf8a0
|
||||||
size 153595
|
size 159902
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:999e8810095d712fd3e1d9aec7d1c2e83a51fe0c6f805816a0abf65ffae35948
|
oid sha256:dae65098eaf100bb2b762b5a25b4caa3a366e2780690e9a41a976d0bce707a74
|
||||||
size 155531
|
size 161798
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c811e030816eeff12ec117631f093c98ea467fe76403d3eb156ef10aa295ac79
|
oid sha256:bac9d7d51106483c23a118e85a567aafa466ba0a118eae797bd27e8f69d97417
|
||||||
size 103086
|
size 108070
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5c34f0d2af50da052122137c3220336af0214f08320f669d83baa019606534d7
|
oid sha256:94508db7f079602a32529ba110fd027d9660c75c1d81f38a2e28a9448f12d5cc
|
||||||
size 102908
|
size 107929
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ab0efa7ad7ced4f257adf46064a78cba4b07b61e07610b94372aea205803d85d
|
oid sha256:efe3a163270b708fada60d439d66ae82792fe59ef2a2e6bbaa19f264968e56f1
|
||||||
size 72247
|
size 72035
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ab0efa7ad7ced4f257adf46064a78cba4b07b61e07610b94372aea205803d85d
|
oid sha256:efe3a163270b708fada60d439d66ae82792fe59ef2a2e6bbaa19f264968e56f1
|
||||||
size 72247
|
size 72035
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e8df10b2bbfcf7242bff019ca058bbeb0ed948b830e9db0aeb9516d03201431e
|
oid sha256:b8a1b232998033c6b57f309ae3270216a528a9c6b40243cd00500f867b7d3b38
|
||||||
size 31481
|
size 31526
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e8df10b2bbfcf7242bff019ca058bbeb0ed948b830e9db0aeb9516d03201431e
|
oid sha256:b8a1b232998033c6b57f309ae3270216a528a9c6b40243cd00500f867b7d3b38
|
||||||
size 31481
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:81fc617254014ad63c2325d5832a2b758b4d62c4c3e399d2c9b9352fb9d0e0be
|
oid sha256:29ecf5613971aad7c7890ed563d8b69d3c13ad1b2d9189c6cb3a80af53c6568b
|
||||||
size 150682
|
size 156966
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:29222a03a1d9dab355a9d976e4c116b240e33f1c1dc2d4492ea3a1faa2fefe48
|
oid sha256:270c52cdad79a7495bded4560a8500f4b9266e0af8d3ec0742a42a2fd7c79888
|
||||||
size 152165
|
size 158449
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:391e0f9ec34cef26c6d2df7cbdd4e30b1ea71f4c00edc0bbd96550d98430c391
|
oid sha256:429ace579d36361e16639266d2b14b6a515da57b14a25205c5e2460a1a8fe85e
|
||||||
size 100964
|
size 105969
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:abd4ee010b07107658b3e3aa4e2947fa3c8d55f4b79466f77461819881c2df8b
|
oid sha256:f29a6ad8b2d81b1e3ffe1d9d8b526488d7c021bf17b04e74d8d3db38f2955944
|
||||||
size 102214
|
size 107269
|
||||||
|
|||||||
@@ -425,14 +425,88 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes")
|
XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUserMentionAtachment() {
|
func testUserPermalinkMentionAtachment() {
|
||||||
let string = "https://matrix.to/#/@test:matrix.org"
|
let string = "https://matrix.to/#/@test:matrix.org"
|
||||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||||
XCTAssertNotNil(attributedStringFromHTML?.link)
|
XCTAssertEqual(attributedStringFromHTML?.userID, "@test:matrix.org")
|
||||||
|
XCTAssertEqual(attributedStringFromHTML?.link?.absoluteString, string)
|
||||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
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() {
|
func testUserMentionAtachmentInBlockQuotes() {
|
||||||
@@ -631,7 +705,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
XCTAssertEqual(foundLink, url)
|
XCTAssertEqual(foundLink, url)
|
||||||
XCTAssertEqual(foundAttachments, 2)
|
XCTAssertEqual(foundAttachments, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class HomeScreenRoomTests: XCTestCase {
|
|||||||
unreadNotificationsCount: unreadNotificationsCount,
|
unreadNotificationsCount: unreadNotificationsCount,
|
||||||
notificationMode: notificationMode,
|
notificationMode: notificationMode,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: hasOngoingCall,
|
hasOngoingCall: hasOngoingCall,
|
||||||
isMarkedUnread: isMarkedUnread,
|
isMarkedUnread: isMarkedUnread,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class LoggingTests: XCTestCase {
|
|||||||
unreadNotificationsCount: 0,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import XCTest
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class PillContextTests: XCTestCase {
|
class PillContextTests: XCTestCase {
|
||||||
func testUser() async throws {
|
func testUser() async {
|
||||||
let id = "@test:matrix.org"
|
let id = "@test:matrix.org"
|
||||||
let proxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
let proxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||||
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||||
@@ -27,7 +27,8 @@ class PillContextTests: XCTestCase {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
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)))
|
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.isOwnMention)
|
XCTAssertFalse(context.viewState.isOwnMention)
|
||||||
@@ -39,10 +40,11 @@ class PillContextTests: XCTestCase {
|
|||||||
await Task.yield()
|
await Task.yield()
|
||||||
|
|
||||||
XCTAssertFalse(context.viewState.isOwnMention)
|
XCTAssertFalse(context.viewState.isOwnMention)
|
||||||
|
XCTAssertNil(context.viewState.image)
|
||||||
XCTAssertEqual(context.viewState.displayText, "@\(name)")
|
XCTAssertEqual(context.viewState.displayText, "@\(name)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOwnUser() async throws {
|
func testOwnUser() {
|
||||||
let id = "@test:matrix.org"
|
let id = "@test:matrix.org"
|
||||||
let proxyMock = JoinedRoomProxyMock(.init(name: "Test", ownUserID: id))
|
let proxyMock = JoinedRoomProxyMock(.init(name: "Test", ownUserID: id))
|
||||||
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||||
@@ -57,13 +59,15 @@ class PillContextTests: XCTestCase {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
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)))
|
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
|
||||||
|
|
||||||
|
XCTAssertNil(context.viewState.image)
|
||||||
XCTAssertTrue(context.viewState.isOwnMention)
|
XCTAssertTrue(context.viewState.isOwnMention)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAllUsers() async throws {
|
func testAllUsers() {
|
||||||
let avatarURL = URL(string: "https://matrix.jpg")
|
let avatarURL = URL(string: "https://matrix.jpg")
|
||||||
let id = "test_room"
|
let id = "test_room"
|
||||||
let displayName = "Test"
|
let displayName = "Test"
|
||||||
@@ -80,10 +84,216 @@ class PillContextTests: XCTestCase {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
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)))
|
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
|
||||||
|
|
||||||
XCTAssertTrue(context.viewState.isOwnMention)
|
XCTAssertTrue(context.viewState.isOwnMention)
|
||||||
|
XCTAssertNil(context.viewState.image)
|
||||||
XCTAssertEqual(context.viewState.displayText, PillConstants.atRoom)
|
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,
|
unreadNotificationsCount: 0,
|
||||||
notificationMode: nil,
|
notificationMode: nil,
|
||||||
canonicalAlias: nil,
|
canonicalAlias: nil,
|
||||||
|
alternativeAliases: [],
|
||||||
hasOngoingCall: false,
|
hasOngoingCall: false,
|
||||||
isMarkedUnread: false,
|
isMarkedUnread: false,
|
||||||
isFavourite: false)
|
isFavourite: false)
|
||||||
|
|||||||
@@ -312,7 +312,8 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
return (viewModel, roomProxy, timelineProxy, timelineController)
|
return (viewModel, roomProxy, timelineProxy, timelineController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +339,8 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
|
|
||||||
let deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
let deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
||||||
value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts
|
value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts
|
||||||
@@ -367,7 +369,8 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
XCTAssertEqual(configuration.pinnedEventIDs, viewModel.context.viewState.pinnedEventIDs)
|
XCTAssertEqual(configuration.pinnedEventIDs, viewModel.context.viewState.pinnedEventIDs)
|
||||||
|
|
||||||
configuration.pinnedEventIDs = ["test1", "test2"]
|
configuration.pinnedEventIDs = ["test1", "test2"]
|
||||||
@@ -394,7 +397,8 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
|
|
||||||
var deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
var deferred = deferFulfillment(viewModel.context.$viewState) { value in
|
||||||
value.canCurrentUserPin
|
value.canCurrentUserPin
|
||||||
@@ -425,7 +429,8 @@ class TimelineViewModelTests: XCTestCase {
|
|||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||||
|
clientProxy: ClientProxyMock(.init()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user