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