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)