// // Copyright 2023 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation @available (iOS 15.0, *) private enum PillAttachmentKind { case attachment(PillTextAttachment) case string(NSAttributedString) } @available (iOS 15.0, *) struct PillProvider { private let session: MXSession private let eventFormatter: MXKEventFormatter private let event: MXEvent private let roomState: MXRoomState private let latestRoomState: MXRoomState? private let isEditMode: Bool init(withSession session: MXSession, eventFormatter: MXKEventFormatter, event: MXEvent, roomState: MXRoomState, andLatestRoomState latestRoomState: MXRoomState?, isEditMode: Bool) { self.session = session self.eventFormatter = eventFormatter self.event = event self.roomState = roomState self.latestRoomState = latestRoomState self.isEditMode = isEditMode } func pillTextAttachmentString(forUrl url: URL, withLabel label: String, event: MXEvent) -> NSAttributedString? { // Try to get a pill from this url guard let pillType = PillType.from(url: url) else { return nil } // Do not pillify an url if it is a markdown or an http link (except for user and room) with a custom text let urlFromLabel = URL(string: label)?.absoluteURL let isUrlMarkDownLink = urlFromLabel != url let result: PillAttachmentKind switch pillType { case .user(let userId): var userFound = false result = pillTextAttachment(forUserId: userId, userFound: &userFound) // if it is a markdown link and we didn't found the user, don't pillify it if isUrlMarkDownLink && !userFound { return nil } case .room(let roomId): var roomFound = false result = pillTextAttachment(forRoomId: roomId, roomFound: &roomFound) // if it is a markdown link and we didn't found the room, don't pillify it if isUrlMarkDownLink && !roomFound { return nil } case .message(let roomId, let messageId): // if it is a markdown link, don't pillify it if isUrlMarkDownLink { return nil } result = pillTextAttachment(forMessageId: messageId, inRoomId: roomId) } switch result { case .attachment(let pillTextAttachment): return PillsFormatter.attributedStringWithAttachment(pillTextAttachment, link: isEditMode ? nil : url, font: eventFormatter.defaultTextFont) case .string(let attributedString): // if we don't have an attachment, use the fallback attributed string let newAttrString = NSMutableAttributedString(attributedString: attributedString) if let font = eventFormatter.defaultTextFont { newAttrString.addAttribute(.font, value: font, range: .init(location: 0, length: newAttrString.length)) } newAttrString.addAttribute(.foregroundColor, value: ThemeService.shared().theme.colors.links, range: .init(location: 0, length: newAttrString.length)) newAttrString.addAttribute(.link, value: url, range: .init(location: 0, length: newAttrString.length)) return newAttrString } } /// Retrieve the latest available `MXRoomMember` from given data. /// /// - Parameters: /// - userId: the id of the user /// - Returns: the room member, if available private func roomMember(withUserId userId: String) -> MXRoomMember? { return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId) } /// Create a pill representation for a given user /// - Parameters: /// - userId: the user MatrixID /// - userFound: this flag will be set to true if a user is found locally with this userId /// - Returns: a pill attachment private func pillTextAttachment(forUserId userId: String, userFound: inout Bool) -> PillAttachmentKind { // Search for a room member matching this user id let roomMember = self.roomMember(withUserId: userId) var user: MXUser? if roomMember == nil { // fallback on getting the user from the session's store user = session.user(withUserId: userId) } let avatarUrl = roomMember?.avatarUrl ?? user?.avatarUrl let displayName = roomMember?.displayname ?? user?.displayName ?? userId let isHighlighted = userId == session.myUserId let avatar: PillTextAttachmentItem if avatarUrl == nil { avatar = .asset(named: "pill_user", parameters: .init(tintColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.secondaryContent), rawRenderingMode: UIImage.RenderingMode.alwaysOriginal.rawValue, padding: 0.0)) } else { avatar = .avatar(url: avatarUrl, string: displayName, matrixId: userId) } let data = PillTextAttachmentData(pillType: .user(userId: userId), items: [ avatar, .text(displayName) ], isHighlighted: isHighlighted, alpha: 1.0, font: eventFormatter.defaultTextFont) userFound = roomMember != nil || user != nil if let attachment = PillTextAttachment(attachmentData: data) { return .attachment(attachment) } return .string(NSMutableAttributedString(string: displayName)) } /// Create a pill representation for a given room /// - Parameters: /// - roomId: the room MXID or alias /// - roomFound: this flag will be set to true if a room is found locally with this roomId /// - Returns: a pill attachment private func pillTextAttachment(forRoomId roomId: String, roomFound: inout Bool) -> PillAttachmentKind { // Get the room matching this roomId let room = roomId.starts(with: "#") ? session.room(withAlias: roomId) : session.room(withRoomId: roomId) let displayName = room?.displayName ?? VectorL10n.pillRoomFallbackDisplayName let avatar: PillTextAttachmentItem if let room { if session.spaceService.getSpace(withId: roomId) != nil { avatar = .spaceAvatar(url: room.avatarData.mxContentUri, string: displayName, matrixId: roomId) } else { avatar = .avatar(url: room.avatarData.mxContentUri, string: displayName, matrixId: roomId) } } else { avatar = .asset(named: "link_icon", parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links), rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue)) } let data = PillTextAttachmentData(pillType: .room(roomId: roomId), items: [ avatar, .text(displayName) ], isHighlighted: false, alpha: 1.0, font: eventFormatter.defaultTextFont) roomFound = room != nil if let attachment = PillTextAttachment(attachmentData: data) { return .attachment(attachment) } return .string(NSMutableAttributedString(string: displayName)) } /// Create a pill representation for a message in a room /// - Parameters: /// - messageId: message eventId /// - roomId: roomId of the message /// - Returns: a pill attachment private func pillTextAttachment(forMessageId messageId: String, inRoomId roomId: String) -> PillAttachmentKind { // Check if this is the current room if roomId == roomState.roomId { return pillTextAttachment(inCurrentRoomForMessageId: messageId) } let room = session.room(withRoomId: roomId) let avatar: PillTextAttachmentItem if let room { avatar = .avatar(url: room.avatarData.mxContentUri, string: room.displayName, matrixId: roomId) } else { avatar = .asset(named: "link_icon", parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links), rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue)) } func computeDisplayText(withRoomDisplayName displayName: String?) -> String { displayName.flatMap { VectorL10n.pillMessageIn($0) } ?? VectorL10n.pillMessage } let displayText = computeDisplayText(withRoomDisplayName: room?.displayName) let data = PillTextAttachmentData(pillType: .message(roomId: roomId, eventId: messageId), items: [ avatar, .text(displayText) ], isHighlighted: false, alpha: 1.0, font: eventFormatter.defaultTextFont) if let attachment = PillTextAttachment(attachmentData: data) { return .attachment(attachment) } return .string(NSMutableAttributedString(string: displayText)) } /// Create a pill representation for a message in the current room /// - Parameters: /// - messageId: message eventId /// - Returns: a pill attachment private func pillTextAttachment(inCurrentRoomForMessageId messageId: String) -> PillAttachmentKind { var roomMember: MXRoomMember? // Try to get the room member if let event = session.store.event(withEventId: messageId, inRoom: roomState.roomId) { roomMember = self.roomMember(withUserId: event.sender) } let displayText: String if let userDisplayName = roomMember?.displayname { displayText = VectorL10n.pillMessageFrom(userDisplayName) } else { displayText = VectorL10n.pillMessage } let avatarUrl = roomMember?.avatarUrl let data = PillTextAttachmentData(pillType: .message(roomId: roomState.roomId, eventId: messageId), items: [ .avatar(url: avatarUrl, string: roomMember?.displayname, matrixId: roomMember?.userId ?? ""), .text(displayText) ].compactMap { $0 }, isHighlighted: false, alpha: 1.0, font: eventFormatter.defaultTextFont) if let attachment = PillTextAttachment(attachmentData: data) { return .attachment(attachment) } return .string(NSMutableAttributedString(string: displayText)) } }