mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-01 13:46:57 +02:00
Merge pull request #7432 from vector-im/nimau/PSB-59-pills
Turning permalinks into pills
This commit is contained in:
@@ -36,6 +36,10 @@
|
||||
// Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string.
|
||||
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";
|
||||
|
||||
// Regex expression for permalink detection
|
||||
NSString *const kMXKToolsRegexStringForPermalink = @"\\/#\\/(?:(?:room|user)\\/)?([^\\s]*)";
|
||||
|
||||
|
||||
#pragma mark - MXKTools static private members
|
||||
// The regex used to find matrix ids.
|
||||
static NSRegularExpression *userIdRegex;
|
||||
@@ -47,6 +51,8 @@ static NSRegularExpression *httpLinksRegex;
|
||||
// A regex to find all HTML tags
|
||||
static NSRegularExpression *htmlTagsRegex;
|
||||
static NSDataDetector *linkDetector;
|
||||
// A regex to detect permalinks
|
||||
static NSRegularExpression* permalinkRegex;
|
||||
|
||||
@implementation MXKTools
|
||||
|
||||
@@ -63,6 +69,9 @@ static NSDataDetector *linkDetector;
|
||||
httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
|
||||
|
||||
NSString *permalinkPattern = [NSString stringWithFormat:@"%@%@", BuildSettings.clientPermalinkBaseUrl ?: kMXMatrixDotToUrl, kMXKToolsRegexStringForPermalink];
|
||||
permalinkRegex = [NSRegularExpression regularExpressionWithPattern:permalinkPattern options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1039,10 +1048,29 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
{
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex];
|
||||
}
|
||||
|
||||
// Permalinks
|
||||
NSArray* matches = [httpLinksRegex matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
if (matches) {
|
||||
for (NSTextCheckingResult *match in matches)
|
||||
{
|
||||
NSRange matchRange = [match range];
|
||||
|
||||
NSString *link = [mutableAttributedString.string substringWithRange:matchRange];
|
||||
// Handle potential permalinks
|
||||
if ([permalinkRegex numberOfMatchesInString:link options:0 range:NSMakeRange(0, link.length)]) {
|
||||
NSURLComponents *url = [[NSURLComponents new] initWithString:link];
|
||||
if (url.URL)
|
||||
{
|
||||
[mutableAttributedString addAttribute:NSLinkAttributeName value:url.URL range:matchRange];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This allows to check for normal url based links (like https://element.io)
|
||||
// And set back the default link color
|
||||
NSArray *matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
if (matches)
|
||||
{
|
||||
for (NSTextCheckingResult *match in matches)
|
||||
|
||||
@@ -25,7 +25,9 @@ class PillAttachmentView: UIView {
|
||||
struct Sizes {
|
||||
var verticalMargin: CGFloat
|
||||
var horizontalMargin: CGFloat
|
||||
var avatarLeading: CGFloat
|
||||
var avatarSideLength: CGFloat
|
||||
var itemSpacing: CGFloat
|
||||
|
||||
var pillBackgroundHeight: CGFloat {
|
||||
return avatarSideLength + 2 * verticalMargin
|
||||
@@ -33,11 +35,8 @@ class PillAttachmentView: UIView {
|
||||
var pillHeight: CGFloat {
|
||||
return pillBackgroundHeight + 2 * verticalMargin
|
||||
}
|
||||
var displaynameLabelLeading: CGFloat {
|
||||
return avatarSideLength + 2 * horizontalMargin
|
||||
}
|
||||
var totalWidthWithoutLabel: CGFloat {
|
||||
return displaynameLabelLeading + 2 * horizontalMargin
|
||||
return avatarSideLength + 2 * horizontalMargin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,44 +55,111 @@ class PillAttachmentView: UIView {
|
||||
mediaManager: MXMediaManager?,
|
||||
andPillData pillData: PillTextAttachmentData) {
|
||||
self.init(frame: frame)
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = pillData.displayText
|
||||
label.font = pillData.font
|
||||
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
|
||||
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: sizes.pillBackgroundHeight))
|
||||
label.frame = CGRect(x: sizes.displaynameLabelLeading,
|
||||
y: 0,
|
||||
width: labelSize.width,
|
||||
height: sizes.pillBackgroundHeight)
|
||||
|
||||
let stack = UIStackView(frame: frame)
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.spacing = sizes.itemSpacing
|
||||
|
||||
var computedWidth: CGFloat = 0
|
||||
for item in pillData.items {
|
||||
switch item {
|
||||
case .text(let string):
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = string
|
||||
label.font = pillData.font
|
||||
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
stack.addArrangedSubview(label)
|
||||
|
||||
computedWidth += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: sizes.pillBackgroundHeight)).width
|
||||
|
||||
case .avatar(let url, let alt, let matrixId):
|
||||
let avatarView = UserAvatarView(frame: CGRect(origin: .zero, size: CGSize(width: sizes.avatarSideLength, height: sizes.avatarSideLength)))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: matrixId,
|
||||
displayName: alt,
|
||||
avatarUrl: url,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(matrixId, alt)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.addArrangedSubview(avatarView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
avatarView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
|
||||
case .spaceAvatar(let url, let alt, let matrixId):
|
||||
let avatarView = SpaceAvatarView(frame: CGRect(origin: .zero, size: CGSize(width: sizes.avatarSideLength, height: sizes.avatarSideLength)))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: matrixId,
|
||||
displayName: alt,
|
||||
avatarUrl: url,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(matrixId, alt)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.addArrangedSubview(avatarView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
avatarView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
|
||||
case .asset(let name, let parameters):
|
||||
let assetView = UIView(frame: CGRect(x: 0, y: 0, width: sizes.avatarSideLength, height: sizes.avatarSideLength))
|
||||
assetView.backgroundColor = parameters.backgroundColor?.uiColor
|
||||
assetView.layer.cornerRadius = sizes.avatarSideLength / 2
|
||||
assetView.isUserInteractionEnabled = false
|
||||
assetView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let imageView = UIImageView(frame: .zero)
|
||||
imageView.image = ImageAsset(name: name).image.withRenderingMode(UIImage.RenderingMode(rawValue: parameters.rawRenderingMode) ?? .automatic)
|
||||
imageView.tintColor = parameters.tintColor?.uiColor ?? theme.baseIconPrimaryColor
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
|
||||
assetView.vc_addSubViewMatchingParent(imageView, withInsets: UIEdgeInsets(top: parameters.padding, left: parameters.padding, bottom: -parameters.padding, right: -parameters.padding))
|
||||
|
||||
stack.addArrangedSubview(assetView)
|
||||
NSLayoutConstraint.activate([
|
||||
assetView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
assetView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
}
|
||||
}
|
||||
computedWidth += max(0, CGFloat(stack.arrangedSubviews.count - 1) * stack.spacing)
|
||||
|
||||
let leadingStackMargin: CGFloat
|
||||
switch pillData.items.first {
|
||||
case .asset, .avatar:
|
||||
leadingStackMargin = sizes.avatarLeading
|
||||
computedWidth += sizes.avatarLeading + sizes.horizontalMargin
|
||||
default:
|
||||
leadingStackMargin = sizes.horizontalMargin
|
||||
computedWidth += 2 * sizes.horizontalMargin
|
||||
}
|
||||
|
||||
let pillBackgroundView = UIView(frame: CGRect(x: 0,
|
||||
y: sizes.verticalMargin,
|
||||
width: labelSize.width + sizes.totalWidthWithoutLabel,
|
||||
width: computedWidth,
|
||||
height: sizes.pillBackgroundHeight))
|
||||
|
||||
let avatarView = UserAvatarView(frame: CGRect(x: sizes.horizontalMargin,
|
||||
y: sizes.verticalMargin,
|
||||
width: sizes.avatarSideLength,
|
||||
height: sizes.avatarSideLength))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: pillData.matrixItemId,
|
||||
displayName: pillData.displayName,
|
||||
avatarUrl: pillData.avatarUrl,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(pillData.matrixItemId, pillData.displayName)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
|
||||
pillBackgroundView.addSubview(avatarView)
|
||||
pillBackgroundView.addSubview(label)
|
||||
|
||||
pillBackgroundView.vc_addSubViewMatchingParent(stack, withInsets: UIEdgeInsets(top: sizes.verticalMargin, left: leadingStackMargin, bottom: -sizes.verticalMargin, right: -sizes.horizontalMargin))
|
||||
|
||||
pillBackgroundView.backgroundColor = pillData.isHighlighted ? theme.colors.alert : theme.colors.quinaryContent
|
||||
pillBackgroundView.layer.cornerRadius = sizes.pillBackgroundHeight / 2.0
|
||||
|
||||
self.addSubview(pillBackgroundView)
|
||||
self.alpha = pillData.alpha
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Override
|
||||
override var isHidden: Bool {
|
||||
get {
|
||||
|
||||
@@ -20,9 +20,11 @@ import UIKit
|
||||
@available(iOS 15.0, *)
|
||||
@objc class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||
// MARK: - Properties
|
||||
private static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
|
||||
horizontalMargin: 4.0,
|
||||
avatarSideLength: 16.0)
|
||||
static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
|
||||
horizontalMargin: 6.0,
|
||||
avatarLeading: 2.0,
|
||||
avatarSideLength: 16.0,
|
||||
itemSpacing: 4)
|
||||
private weak var messageTextView: MXKMessageTextView?
|
||||
|
||||
// MARK: - Override
|
||||
@@ -47,8 +49,7 @@ import UIKit
|
||||
|
||||
let mainSession = AppDelegate.theDelegate().mxSessions.first as? MXSession
|
||||
|
||||
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: Self.size(forDisplayText: pillData.displayText,
|
||||
andFont: pillData.font)),
|
||||
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: textAttachment.size(forFont: pillData.font)),
|
||||
sizes: Self.pillAttachmentViewSizes,
|
||||
theme: ThemeService.shared().theme,
|
||||
mediaManager: mainSession?.mediaManager,
|
||||
@@ -57,23 +58,3 @@ import UIKit
|
||||
messageTextView?.registerPillView(pillView)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
extension PillAttachmentViewProvider {
|
||||
/// Computes size required to display a pill for given display text.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - displayText: display text for the pill
|
||||
/// - font: the text font
|
||||
/// - Returns: required size for pill
|
||||
static func size(forDisplayText displayText: String, andFont font: UIFont) -> CGSize {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = displayText
|
||||
label.font = font
|
||||
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: pillAttachmentViewSizes.pillBackgroundHeight))
|
||||
|
||||
return CGSize(width: labelSize.width + pillAttachmentViewSizes.totalWidthWithoutLabel,
|
||||
height: pillAttachmentViewSizes.pillHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
// First, we need to handle the case where the label can contains more than one # (room alias)
|
||||
var urlFromLabel = URL(string: label)?.absoluteURL
|
||||
if urlFromLabel == nil, label.filter({ $0 == "#" }).count > 1 {
|
||||
if let escapedLabel = label.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedLabel) {
|
||||
urlFromLabel = Tools.fixURL(withSeveralHashKeys: url)
|
||||
}
|
||||
}
|
||||
|
||||
let fixedUrl = Tools.fixURL(withSeveralHashKeys: url)
|
||||
let isUrlMarkDownLink = urlFromLabel != fixedUrl
|
||||
|
||||
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 roomMember == nil && user == 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))
|
||||
|
||||
}
|
||||
|
||||
let displayText = room?.displayName.flatMap { VectorL10n.pillMessageIn($0) } ?? VectorL10n.pillMessage
|
||||
|
||||
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?
|
||||
// If we have the event locally, 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
|
||||
let avatar: PillTextAttachmentItem
|
||||
if let roomMember {
|
||||
displayText = VectorL10n.pillMessageFrom(roomMember.displayname)
|
||||
avatar = .avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId)
|
||||
} else {
|
||||
displayText = VectorL10n.pillMessage
|
||||
avatar = .asset(named: "link_icon",
|
||||
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
|
||||
}
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .message(roomId: roomState.roomId, eventId: messageId),
|
||||
items: [
|
||||
avatar,
|
||||
.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))
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ class PillTextAttachment: NSTextAttachment {
|
||||
|
||||
updateBounds()
|
||||
}
|
||||
|
||||
convenience init?(attachmentData: PillTextAttachmentData) {
|
||||
guard let encodedData = try? Self.serializationService.serialize(attachmentData) else { return nil }
|
||||
self.init(data: encodedData, ofType: PillsFormatter.pillUTType)
|
||||
}
|
||||
|
||||
/// Create a Mention Pill text attachment for given room member.
|
||||
///
|
||||
@@ -55,9 +60,13 @@ class PillTextAttachment: NSTextAttachment {
|
||||
convenience init?(withRoomMember roomMember: MXRoomMember,
|
||||
isHighlighted: Bool,
|
||||
font: UIFont) {
|
||||
let data = PillTextAttachmentData(matrixItemId: roomMember.userId,
|
||||
displayName: roomMember.displayname,
|
||||
avatarUrl: roomMember.avatarUrl,
|
||||
let data = PillTextAttachmentData(pillType: .user(userId: roomMember.userId),
|
||||
items: [
|
||||
.avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId),
|
||||
.text(roomMember.displayname)
|
||||
],
|
||||
isHighlighted: isHighlighted,
|
||||
alpha: 1.0,
|
||||
font: font)
|
||||
@@ -71,14 +80,63 @@ class PillTextAttachment: NSTextAttachment {
|
||||
|
||||
updateBounds()
|
||||
}
|
||||
|
||||
/// Computes size required to display a pill for given display text.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - font: the text font
|
||||
/// - Returns: required size for pill
|
||||
func size(forFont font: UIFont) -> CGSize {
|
||||
guard let data else {
|
||||
MXLog.debug("[PillTextAttachment]: data are missing")
|
||||
return .zero
|
||||
}
|
||||
|
||||
let sizes = PillAttachmentViewProvider.pillAttachmentViewSizes
|
||||
|
||||
var width: CGFloat = 0
|
||||
|
||||
var textContent = ""
|
||||
for item in data.items {
|
||||
switch item {
|
||||
case .text(let text):
|
||||
textContent += text
|
||||
case .avatar, .asset, .spaceAvatar:
|
||||
width += sizes.avatarSideLength
|
||||
}
|
||||
}
|
||||
|
||||
// add texts
|
||||
if !textContent.isEmpty {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = font
|
||||
label.text = textContent
|
||||
width += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: sizes.pillBackgroundHeight)).width
|
||||
}
|
||||
|
||||
// add spacing
|
||||
width += CGFloat(max(0, data.items.count - 1)) * sizes.itemSpacing
|
||||
// add margins
|
||||
switch data.items.first {
|
||||
case .asset, .avatar:
|
||||
width += sizes.avatarLeading + sizes.horizontalMargin
|
||||
default:
|
||||
width += 2 * sizes.horizontalMargin
|
||||
}
|
||||
|
||||
return CGSize(width: width,
|
||||
height: sizes.pillHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@available (iOS 15.0, *)
|
||||
private extension PillTextAttachment {
|
||||
|
||||
func updateBounds() {
|
||||
guard let data = data else { return }
|
||||
let pillSize = PillAttachmentViewProvider.size(forDisplayText: data.displayText, andFont: data.font)
|
||||
let pillSize = size(forFont: data.font)
|
||||
// Offset to align pill centerY with text centerY.
|
||||
let offset = data.font.descender + (data.font.lineHeight - pillSize.height) / 2.0
|
||||
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: pillSize)
|
||||
|
||||
@@ -17,16 +17,55 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
struct PillAssetColor: Codable {
|
||||
var red: CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0
|
||||
|
||||
var uiColor: UIColor {
|
||||
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
|
||||
init(uiColor: UIColor) {
|
||||
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||
}
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
struct PillAssetParameter: Codable {
|
||||
var tintColor: PillAssetColor?
|
||||
var backgroundColor: PillAssetColor?
|
||||
var rawRenderingMode: Int = UIImage.RenderingMode.automatic.rawValue
|
||||
var padding: CGFloat = 2.0
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
enum PillTextAttachmentItem: Codable {
|
||||
case text(String)
|
||||
case avatar(url: String?, string: String?, matrixId: String)
|
||||
case spaceAvatar(url: String?, string: String?, matrixId: String)
|
||||
case asset(named: String, parameters: PillAssetParameter)
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
extension PillTextAttachmentItem {
|
||||
var string: String? {
|
||||
switch self {
|
||||
case .text(let text):
|
||||
return text
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data associated with a Pill text attachment.
|
||||
@available (iOS 15.0, *)
|
||||
struct PillTextAttachmentData: Codable {
|
||||
// MARK: - Properties
|
||||
/// Matrix item identifier (user id or room id)
|
||||
var matrixItemId: String
|
||||
/// Matrix item display name (user or room display name)
|
||||
var displayName: String?
|
||||
/// Matrix item avatar URL (user or room avatar url)
|
||||
var avatarUrl: String?
|
||||
/// Pill type
|
||||
var pillType: PillType
|
||||
/// Items to render
|
||||
var items: [PillTextAttachmentItem]
|
||||
/// Wether the pill should be highlighted
|
||||
var isHighlighted: Bool
|
||||
/// Alpha for pill display
|
||||
@@ -36,43 +75,36 @@ struct PillTextAttachmentData: Codable {
|
||||
|
||||
/// Helper for preferred text to display.
|
||||
var displayText: String {
|
||||
guard let displayName = displayName,
|
||||
displayName.count > 0 else {
|
||||
return matrixItemId
|
||||
}
|
||||
|
||||
return displayName
|
||||
return items.map { $0.string }
|
||||
.compactMap { $0 }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
/// Init.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - matrixItemId: Matrix item identifier (user id or room id)
|
||||
/// - displayName: Matrix item display name (user or room display name)
|
||||
/// - avatarUrl: Matrix item avatar URL (user or room avatar url)
|
||||
/// - pillType: Type for the pill
|
||||
/// - items: Items to display
|
||||
/// - isHighlighted: Wether the pill should be highlighted
|
||||
/// - alpha: Alpha for pill display
|
||||
/// - font: Font for the display name
|
||||
init(matrixItemId: String,
|
||||
displayName: String?,
|
||||
avatarUrl: String?,
|
||||
init(pillType: PillType,
|
||||
items: [PillTextAttachmentItem],
|
||||
isHighlighted: Bool,
|
||||
alpha: CGFloat,
|
||||
font: UIFont) {
|
||||
self.matrixItemId = matrixItemId
|
||||
self.displayName = displayName
|
||||
self.avatarUrl = avatarUrl
|
||||
self.pillType = pillType
|
||||
self.items = items
|
||||
self.isHighlighted = isHighlighted
|
||||
self.alpha = alpha
|
||||
self.font = font
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Codable
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case matrixItemId
|
||||
case displayName
|
||||
case avatarUrl
|
||||
case pillType
|
||||
case items
|
||||
case isHighlighted
|
||||
case alpha
|
||||
case font
|
||||
@@ -84,9 +116,8 @@ struct PillTextAttachmentData: Codable {
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
matrixItemId = try container.decode(String.self, forKey: .matrixItemId)
|
||||
displayName = try? container.decode(String.self, forKey: .displayName)
|
||||
avatarUrl = try? container.decode(String.self, forKey: .avatarUrl)
|
||||
pillType = try container.decode(PillType.self, forKey: .pillType)
|
||||
items = try container.decode([PillTextAttachmentItem].self, forKey: .items)
|
||||
isHighlighted = try container.decode(Bool.self, forKey: .isHighlighted)
|
||||
alpha = try container.decode(CGFloat.self, forKey: .alpha)
|
||||
let fontData = try container.decode(Data.self, forKey: .font)
|
||||
@@ -99,12 +130,36 @@ struct PillTextAttachmentData: Codable {
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(matrixItemId, forKey: .matrixItemId)
|
||||
try? container.encode(displayName, forKey: .displayName)
|
||||
try? container.encode(avatarUrl, forKey: .avatarUrl)
|
||||
try container.encode(pillType, forKey: .pillType)
|
||||
try container.encode(items, forKey: .items)
|
||||
try container.encode(isHighlighted, forKey: .isHighlighted)
|
||||
try container.encode(alpha, forKey: .alpha)
|
||||
let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false)
|
||||
try container.encode(fontData, forKey: .font)
|
||||
}
|
||||
|
||||
// MARK: - Pill representations
|
||||
var pillIdentifier: String {
|
||||
switch pillType {
|
||||
case .user(let userId):
|
||||
return userId
|
||||
case .room(let roomId):
|
||||
return roomId
|
||||
case .message(let roomId, let messageId):
|
||||
return "\(roomId)/\(messageId)"
|
||||
}
|
||||
}
|
||||
|
||||
var markdown: String {
|
||||
var permalink: String
|
||||
switch pillType {
|
||||
case .user(let userId):
|
||||
permalink = MXTools.permalinkToUser(withUserId: userId)
|
||||
case .room(let roomId):
|
||||
permalink = MXTools.permalink(toRoom: roomId)
|
||||
case .message(let roomId, let messageId):
|
||||
permalink = MXTools.permalink(toEvent: messageId, inRoom: roomId)
|
||||
}
|
||||
return "[\(displayText)](\(permalink))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// 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, *)
|
||||
enum PillType: Codable {
|
||||
case user(userId: String) /// userId
|
||||
case room(roomId: String) /// roomId
|
||||
case message(roomId: String, eventId: String) // roomId, eventId
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
extension PillType {
|
||||
private static var regexPermalinkTarget: NSRegularExpression? = {
|
||||
let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl ?? kMXMatrixDotToUrl
|
||||
let pattern = #"\#(clientBaseUrl)/#/(?:(?:room|user)/)?((?:@|!|#)[^@!#/?\s]*)/?((?:\$)[^\$/?\s]*)?"#
|
||||
return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
|
||||
}()
|
||||
|
||||
static func from(url: URL) -> PillType? {
|
||||
guard let regex = regexPermalinkTarget else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var link = url.absoluteString
|
||||
// we need to remove percent encoding (it's possible that it has been encoded multiple times)
|
||||
while let cleaned = link.removingPercentEncoding, cleaned != link {
|
||||
link = cleaned
|
||||
}
|
||||
|
||||
let pills = regex.matches(in: link, options: [], range: NSRange(link.startIndex..., in: link))
|
||||
.map { result -> [String]? in
|
||||
guard result.numberOfRanges > 1 else { return nil }
|
||||
return (1..<result.numberOfRanges)
|
||||
.map { Range(result.range(at: $0), in: link) }
|
||||
.compactMap { $0 }
|
||||
.map { String(link[$0]).removingPercentEncoding }
|
||||
.compactMap { $0 }
|
||||
|
||||
}
|
||||
.compactMap { matrixIds -> PillType? in
|
||||
guard let matrixIds, !matrixIds.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
switch matrixIds[0].first {
|
||||
case "@":
|
||||
return .user(userId: matrixIds[0])
|
||||
case "!", "#":
|
||||
if matrixIds.count > 1 {
|
||||
if matrixIds[1].starts(with: "$") {
|
||||
return .message(roomId: matrixIds[0], eventId: matrixIds[1])
|
||||
}
|
||||
}
|
||||
return .room(roomId: matrixIds[0])
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return pills.first
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class PillsFormatter: NSObject {
|
||||
case identifier
|
||||
case markdown
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Internal Methods
|
||||
/// Insert text attachments for pills inside given message attributed string.
|
||||
///
|
||||
@@ -52,17 +52,21 @@ class PillsFormatter: NSObject {
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?,
|
||||
isEditMode: Bool = false) -> NSAttributedString {
|
||||
|
||||
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
||||
newAttr.vc_enumerateAttribute(.link) { (url: URL, range: NSRange, _) in
|
||||
if let userId = userIdFromPermalink(url.absoluteString),
|
||||
let roomMember = roomMember(withUserId: userId,
|
||||
roomState: roomState,
|
||||
andLatestRoomState: latestRoomState) {
|
||||
let isHighlighted = roomMember.userId == session.myUserId && event.sender != session.myUserId
|
||||
let attachmentString = mentionPill(withRoomMember: roomMember,
|
||||
andUrl: isEditMode ? nil : url,
|
||||
isHighlighted: isHighlighted,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
let provider = PillProvider(withSession: session,
|
||||
eventFormatter: eventFormatter,
|
||||
event: event,
|
||||
roomState: roomState,
|
||||
andLatestRoomState: latestRoomState,
|
||||
isEditMode: isEditMode)
|
||||
|
||||
// try to get a mention pill from the url
|
||||
let label = Range(range, in: newAttr.string).flatMap { String(newAttr.string[$0]) }
|
||||
if let attachmentString: NSAttributedString = provider.pillTextAttachmentString(forUrl: url, withLabel: label ?? "", event: event) {
|
||||
// replace the url with the pill
|
||||
newAttr.replaceCharacters(in: range, with: attachmentString)
|
||||
}
|
||||
}
|
||||
@@ -80,25 +84,27 @@ class PillsFormatter: NSObject {
|
||||
mode: PillsReplacementTextMode = .displayname) -> String {
|
||||
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
||||
newAttr.vc_enumerateAttribute(.attachment) { (attachment: PillTextAttachment, range: NSRange, _) in
|
||||
if let displayText = attachment.data?.displayText,
|
||||
let userId = attachment.data?.matrixItemId,
|
||||
let permalink = MXTools.permalinkToUser(withUserId: userId) {
|
||||
let pillString: String
|
||||
switch mode {
|
||||
case .displayname:
|
||||
pillString = displayText
|
||||
case .identifier:
|
||||
pillString = userId
|
||||
case .markdown:
|
||||
pillString = "[\(displayText)](\(permalink))"
|
||||
}
|
||||
newAttr.replaceCharacters(in: range, with: pillString)
|
||||
guard let data = attachment.data else {
|
||||
return
|
||||
}
|
||||
|
||||
let pillString: String
|
||||
switch mode {
|
||||
case .displayname:
|
||||
pillString = data.displayText
|
||||
case .identifier:
|
||||
pillString = data.pillIdentifier
|
||||
case .markdown:
|
||||
pillString = data.markdown
|
||||
}
|
||||
|
||||
newAttr.replaceCharacters(in: range, with: pillString)
|
||||
}
|
||||
|
||||
return newAttr.string
|
||||
}
|
||||
|
||||
|
||||
/// Creates an attributed string containing a pill for given room member.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -111,17 +117,13 @@ class PillsFormatter: NSObject {
|
||||
andUrl url: URL? = nil,
|
||||
isHighlighted: Bool,
|
||||
font: UIFont) -> NSAttributedString {
|
||||
|
||||
guard let attachment = PillTextAttachment(withRoomMember: roomMember, isHighlighted: isHighlighted, font: font) else {
|
||||
return NSAttributedString(string: roomMember.displayname)
|
||||
}
|
||||
let string = NSMutableAttributedString(attachment: attachment)
|
||||
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
|
||||
if let url = url {
|
||||
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
|
||||
}
|
||||
return string
|
||||
return attributedStringWithAttachment(attachment, link: url, font: font)
|
||||
}
|
||||
|
||||
|
||||
/// Update alpha of all `PillTextAttachment` contained in given attributed string.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -140,43 +142,37 @@ class PillsFormatter: NSObject {
|
||||
/// - roomState: room state for refresh, should be the latest available
|
||||
static func refreshPills(in attributedString: NSAttributedString, with roomState: MXRoomState) {
|
||||
attributedString.vc_enumerateAttribute(.attachment) { (pill: PillTextAttachment, range: NSRange, _) in
|
||||
guard let userId = pill.data?.matrixItemId,
|
||||
let roomMember = roomState.members.member(withUserId: userId) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch pill.data?.pillType {
|
||||
case .user(let userId):
|
||||
guard let roomMember = roomState.members.member(withUserId: userId) else {
|
||||
return
|
||||
}
|
||||
|
||||
pill.data?.displayName = roomMember.displayname
|
||||
pill.data?.avatarUrl = roomMember.avatarUrl
|
||||
pill.data?.items = [
|
||||
.avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId),
|
||||
.text(roomMember.displayname)
|
||||
]
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
@available (iOS 15.0, *)
|
||||
private extension PillsFormatter {
|
||||
/// Extract user id from given permalink
|
||||
/// - Parameter permalink: the permalink
|
||||
/// - Returns: userId, if any
|
||||
static func userIdFromPermalink(_ permalink: String) -> String? {
|
||||
let baseUrl: String
|
||||
if let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl {
|
||||
baseUrl = String(format: "%@/#/user/", clientBaseUrl)
|
||||
} else {
|
||||
baseUrl = String(format: "%@/#/", kMXMatrixDotToUrl)
|
||||
extension PillsFormatter {
|
||||
|
||||
static func attributedStringWithAttachment(_ attachment: PillTextAttachment, link: URL?, font: UIFont) -> NSAttributedString {
|
||||
let string = NSMutableAttributedString(attachment: attachment)
|
||||
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
|
||||
if let url = link {
|
||||
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
|
||||
}
|
||||
return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil
|
||||
}
|
||||
|
||||
/// Retrieve the latest available `MXRoomMember` from given data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userId: the id of the user
|
||||
/// - roomState: room state for message
|
||||
/// - latestRoomState: latest room state of the room containing this message
|
||||
/// - Returns: the room member, if available
|
||||
static func roomMember(withUserId userId: String,
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?) -> MXRoomMember? {
|
||||
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ final class SpaceAvatarView: AvatarView, NibOwnerLoadable {
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.avatarImageView.layer.cornerRadius = Constants.cornerRadius
|
||||
// Ensure we keep a rounded corner if the width is less than 2 * Constants.cornerRadius
|
||||
self.avatarImageView.layer.cornerRadius = max(2.0, min(self.avatarImageView.bounds.width / 4, Constants.cornerRadius))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@@ -23,6 +23,7 @@ final class UserAvatarView: AvatarView {
|
||||
|
||||
private func commonInit() {
|
||||
let avatarImageView = MXKImageView()
|
||||
avatarImageView.frame = self.frame
|
||||
self.vc_addSubViewMatchingParent(avatarImageView)
|
||||
self.avatarImageView = avatarImageView
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user