mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-02 06:06:57 +02:00
Merge pull request #7432 from vector-im/nimau/PSB-59-pills
Turning permalinks into pills
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "pill_user.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "pill_user@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "pill_user@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 754 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -3156,3 +3156,9 @@ To enable access, tap Settings> Location and select Always";
|
|||||||
"ssl_unexpected_existing_expl" = "The certificate has changed from one that was trusted by your phone. This is HIGHLY UNUSUAL. It is recommended that you DO NOT ACCEPT this new certificate.";
|
"ssl_unexpected_existing_expl" = "The certificate has changed from one that was trusted by your phone. This is HIGHLY UNUSUAL. It is recommended that you DO NOT ACCEPT this new certificate.";
|
||||||
"ssl_expected_existing_expl" = "The certificate has changed from a previously trusted one to one that is not trusted. The server may have renewed its certificate. Contact the server administrator for the expected fingerprint.";
|
"ssl_expected_existing_expl" = "The certificate has changed from a previously trusted one to one that is not trusted. The server may have renewed its certificate. Contact the server administrator for the expected fingerprint.";
|
||||||
"ssl_only_accept" = "ONLY accept the certificate if the server administrator has published a fingerprint that matches the one above.";
|
"ssl_only_accept" = "ONLY accept the certificate if the server administrator has published a fingerprint that matches the one above.";
|
||||||
|
|
||||||
|
// Pills
|
||||||
|
"pill_room_fallback_display_name" = "Space/Room";
|
||||||
|
"pill_message" = "Message";
|
||||||
|
"pill_message_from" = "Message from %@";
|
||||||
|
"pill_message_in" = "Message in %@";
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ internal class Asset: NSObject {
|
|||||||
internal static let locationPinIcon = ImageAsset(name: "location_pin_icon")
|
internal static let locationPinIcon = ImageAsset(name: "location_pin_icon")
|
||||||
internal static let locationShareIcon = ImageAsset(name: "location_share_icon")
|
internal static let locationShareIcon = ImageAsset(name: "location_share_icon")
|
||||||
internal static let locationUserMarker = ImageAsset(name: "location_user_marker")
|
internal static let locationUserMarker = ImageAsset(name: "location_user_marker")
|
||||||
|
internal static let pillUser = ImageAsset(name: "pill_user")
|
||||||
internal static let pollCheckboxDefault = ImageAsset(name: "poll_checkbox_default")
|
internal static let pollCheckboxDefault = ImageAsset(name: "poll_checkbox_default")
|
||||||
internal static let pollCheckboxSelected = ImageAsset(name: "poll_checkbox_selected")
|
internal static let pollCheckboxSelected = ImageAsset(name: "poll_checkbox_selected")
|
||||||
internal static let pollDeleteIcon = ImageAsset(name: "poll_delete_icon")
|
internal static let pollDeleteIcon = ImageAsset(name: "poll_delete_icon")
|
||||||
|
|||||||
@@ -4691,6 +4691,22 @@ public class VectorL10n: NSObject {
|
|||||||
public static func photoLibraryAccessNotGranted(_ p1: String) -> String {
|
public static func photoLibraryAccessNotGranted(_ p1: String) -> String {
|
||||||
return VectorL10n.tr("Vector", "photo_library_access_not_granted", p1)
|
return VectorL10n.tr("Vector", "photo_library_access_not_granted", p1)
|
||||||
}
|
}
|
||||||
|
/// Message
|
||||||
|
public static var pillMessage: String {
|
||||||
|
return VectorL10n.tr("Vector", "pill_message")
|
||||||
|
}
|
||||||
|
/// Message from %@
|
||||||
|
public static func pillMessageFrom(_ p1: String) -> String {
|
||||||
|
return VectorL10n.tr("Vector", "pill_message_from", p1)
|
||||||
|
}
|
||||||
|
/// Message in %@
|
||||||
|
public static func pillMessageIn(_ p1: String) -> String {
|
||||||
|
return VectorL10n.tr("Vector", "pill_message_in", p1)
|
||||||
|
}
|
||||||
|
/// Space/Room
|
||||||
|
public static var pillRoomFallbackDisplayName: String {
|
||||||
|
return VectorL10n.tr("Vector", "pill_room_fallback_display_name")
|
||||||
|
}
|
||||||
/// Create a PIN for security
|
/// Create a PIN for security
|
||||||
public static var pinProtectionChoosePin: String {
|
public static var pinProtectionChoosePin: String {
|
||||||
return VectorL10n.tr("Vector", "pin_protection_choose_pin")
|
return VectorL10n.tr("Vector", "pin_protection_choose_pin")
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
// Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string.
|
// Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string.
|
||||||
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";
|
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";
|
||||||
|
|
||||||
|
// Regex expression for permalink detection
|
||||||
|
NSString *const kMXKToolsRegexStringForPermalink = @"\\/#\\/(?:(?:room|user)\\/)?([^\\s]*)";
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - MXKTools static private members
|
#pragma mark - MXKTools static private members
|
||||||
// The regex used to find matrix ids.
|
// The regex used to find matrix ids.
|
||||||
static NSRegularExpression *userIdRegex;
|
static NSRegularExpression *userIdRegex;
|
||||||
@@ -47,6 +51,8 @@ static NSRegularExpression *httpLinksRegex;
|
|||||||
// A regex to find all HTML tags
|
// A regex to find all HTML tags
|
||||||
static NSRegularExpression *htmlTagsRegex;
|
static NSRegularExpression *htmlTagsRegex;
|
||||||
static NSDataDetector *linkDetector;
|
static NSDataDetector *linkDetector;
|
||||||
|
// A regex to detect permalinks
|
||||||
|
static NSRegularExpression* permalinkRegex;
|
||||||
|
|
||||||
@implementation MXKTools
|
@implementation MXKTools
|
||||||
|
|
||||||
@@ -63,6 +69,9 @@ static NSDataDetector *linkDetector;
|
|||||||
httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
|
httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||||
htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
|
htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||||
linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink 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];
|
[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)
|
// This allows to check for normal url based links (like https://element.io)
|
||||||
// And set back the default link color
|
// 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)
|
if (matches)
|
||||||
{
|
{
|
||||||
for (NSTextCheckingResult *match in matches)
|
for (NSTextCheckingResult *match in matches)
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ class PillAttachmentView: UIView {
|
|||||||
struct Sizes {
|
struct Sizes {
|
||||||
var verticalMargin: CGFloat
|
var verticalMargin: CGFloat
|
||||||
var horizontalMargin: CGFloat
|
var horizontalMargin: CGFloat
|
||||||
|
var avatarLeading: CGFloat
|
||||||
var avatarSideLength: CGFloat
|
var avatarSideLength: CGFloat
|
||||||
|
var itemSpacing: CGFloat
|
||||||
|
|
||||||
var pillBackgroundHeight: CGFloat {
|
var pillBackgroundHeight: CGFloat {
|
||||||
return avatarSideLength + 2 * verticalMargin
|
return avatarSideLength + 2 * verticalMargin
|
||||||
@@ -33,11 +35,8 @@ class PillAttachmentView: UIView {
|
|||||||
var pillHeight: CGFloat {
|
var pillHeight: CGFloat {
|
||||||
return pillBackgroundHeight + 2 * verticalMargin
|
return pillBackgroundHeight + 2 * verticalMargin
|
||||||
}
|
}
|
||||||
var displaynameLabelLeading: CGFloat {
|
|
||||||
return avatarSideLength + 2 * horizontalMargin
|
|
||||||
}
|
|
||||||
var totalWidthWithoutLabel: CGFloat {
|
var totalWidthWithoutLabel: CGFloat {
|
||||||
return displaynameLabelLeading + 2 * horizontalMargin
|
return avatarSideLength + 2 * horizontalMargin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,44 +55,111 @@ class PillAttachmentView: UIView {
|
|||||||
mediaManager: MXMediaManager?,
|
mediaManager: MXMediaManager?,
|
||||||
andPillData pillData: PillTextAttachmentData) {
|
andPillData pillData: PillTextAttachmentData) {
|
||||||
self.init(frame: frame)
|
self.init(frame: frame)
|
||||||
let label = UILabel(frame: .zero)
|
|
||||||
label.text = pillData.displayText
|
let stack = UIStackView(frame: frame)
|
||||||
label.font = pillData.font
|
stack.axis = .horizontal
|
||||||
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
|
stack.alignment = .center
|
||||||
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
stack.spacing = sizes.itemSpacing
|
||||||
height: sizes.pillBackgroundHeight))
|
|
||||||
label.frame = CGRect(x: sizes.displaynameLabelLeading,
|
|
||||||
y: 0,
|
|
||||||
width: labelSize.width,
|
|
||||||
height: sizes.pillBackgroundHeight)
|
|
||||||
|
|
||||||
|
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,
|
let pillBackgroundView = UIView(frame: CGRect(x: 0,
|
||||||
y: sizes.verticalMargin,
|
y: sizes.verticalMargin,
|
||||||
width: labelSize.width + sizes.totalWidthWithoutLabel,
|
width: computedWidth,
|
||||||
height: sizes.pillBackgroundHeight))
|
height: sizes.pillBackgroundHeight))
|
||||||
|
|
||||||
let avatarView = UserAvatarView(frame: CGRect(x: sizes.horizontalMargin,
|
pillBackgroundView.vc_addSubViewMatchingParent(stack, withInsets: UIEdgeInsets(top: sizes.verticalMargin, left: leadingStackMargin, bottom: -sizes.verticalMargin, right: -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.backgroundColor = pillData.isHighlighted ? theme.colors.alert : theme.colors.quinaryContent
|
pillBackgroundView.backgroundColor = pillData.isHighlighted ? theme.colors.alert : theme.colors.quinaryContent
|
||||||
pillBackgroundView.layer.cornerRadius = sizes.pillBackgroundHeight / 2.0
|
pillBackgroundView.layer.cornerRadius = sizes.pillBackgroundHeight / 2.0
|
||||||
|
|
||||||
self.addSubview(pillBackgroundView)
|
self.addSubview(pillBackgroundView)
|
||||||
self.alpha = pillData.alpha
|
self.alpha = pillData.alpha
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Override
|
// MARK: - Override
|
||||||
override var isHidden: Bool {
|
override var isHidden: Bool {
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import UIKit
|
|||||||
@available(iOS 15.0, *)
|
@available(iOS 15.0, *)
|
||||||
@objc class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
@objc class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
private static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
|
static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
|
||||||
horizontalMargin: 4.0,
|
horizontalMargin: 6.0,
|
||||||
avatarSideLength: 16.0)
|
avatarLeading: 2.0,
|
||||||
|
avatarSideLength: 16.0,
|
||||||
|
itemSpacing: 4)
|
||||||
private weak var messageTextView: MXKMessageTextView?
|
private weak var messageTextView: MXKMessageTextView?
|
||||||
|
|
||||||
// MARK: - Override
|
// MARK: - Override
|
||||||
@@ -47,8 +49,7 @@ import UIKit
|
|||||||
|
|
||||||
let mainSession = AppDelegate.theDelegate().mxSessions.first as? MXSession
|
let mainSession = AppDelegate.theDelegate().mxSessions.first as? MXSession
|
||||||
|
|
||||||
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: Self.size(forDisplayText: pillData.displayText,
|
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: textAttachment.size(forFont: pillData.font)),
|
||||||
andFont: pillData.font)),
|
|
||||||
sizes: Self.pillAttachmentViewSizes,
|
sizes: Self.pillAttachmentViewSizes,
|
||||||
theme: ThemeService.shared().theme,
|
theme: ThemeService.shared().theme,
|
||||||
mediaManager: mainSession?.mediaManager,
|
mediaManager: mainSession?.mediaManager,
|
||||||
@@ -57,23 +58,3 @@ import UIKit
|
|||||||
messageTextView?.registerPillView(pillView)
|
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()
|
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.
|
/// Create a Mention Pill text attachment for given room member.
|
||||||
///
|
///
|
||||||
@@ -55,9 +60,13 @@ class PillTextAttachment: NSTextAttachment {
|
|||||||
convenience init?(withRoomMember roomMember: MXRoomMember,
|
convenience init?(withRoomMember roomMember: MXRoomMember,
|
||||||
isHighlighted: Bool,
|
isHighlighted: Bool,
|
||||||
font: UIFont) {
|
font: UIFont) {
|
||||||
let data = PillTextAttachmentData(matrixItemId: roomMember.userId,
|
let data = PillTextAttachmentData(pillType: .user(userId: roomMember.userId),
|
||||||
displayName: roomMember.displayname,
|
items: [
|
||||||
avatarUrl: roomMember.avatarUrl,
|
.avatar(url: roomMember.avatarUrl,
|
||||||
|
string: roomMember.displayname,
|
||||||
|
matrixId: roomMember.userId),
|
||||||
|
.text(roomMember.displayname)
|
||||||
|
],
|
||||||
isHighlighted: isHighlighted,
|
isHighlighted: isHighlighted,
|
||||||
alpha: 1.0,
|
alpha: 1.0,
|
||||||
font: font)
|
font: font)
|
||||||
@@ -71,14 +80,63 @@ class PillTextAttachment: NSTextAttachment {
|
|||||||
|
|
||||||
updateBounds()
|
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
|
// MARK: - Private
|
||||||
@available (iOS 15.0, *)
|
@available (iOS 15.0, *)
|
||||||
private extension PillTextAttachment {
|
private extension PillTextAttachment {
|
||||||
|
|
||||||
func updateBounds() {
|
func updateBounds() {
|
||||||
guard let data = data else { return }
|
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.
|
// Offset to align pill centerY with text centerY.
|
||||||
let offset = data.font.descender + (data.font.lineHeight - pillSize.height) / 2.0
|
let offset = data.font.descender + (data.font.lineHeight - pillSize.height) / 2.0
|
||||||
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: pillSize)
|
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: pillSize)
|
||||||
|
|||||||
@@ -17,16 +17,55 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
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.
|
/// Data associated with a Pill text attachment.
|
||||||
@available (iOS 15.0, *)
|
@available (iOS 15.0, *)
|
||||||
struct PillTextAttachmentData: Codable {
|
struct PillTextAttachmentData: Codable {
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
/// Matrix item identifier (user id or room id)
|
/// Pill type
|
||||||
var matrixItemId: String
|
var pillType: PillType
|
||||||
/// Matrix item display name (user or room display name)
|
/// Items to render
|
||||||
var displayName: String?
|
var items: [PillTextAttachmentItem]
|
||||||
/// Matrix item avatar URL (user or room avatar url)
|
|
||||||
var avatarUrl: String?
|
|
||||||
/// Wether the pill should be highlighted
|
/// Wether the pill should be highlighted
|
||||||
var isHighlighted: Bool
|
var isHighlighted: Bool
|
||||||
/// Alpha for pill display
|
/// Alpha for pill display
|
||||||
@@ -36,43 +75,36 @@ struct PillTextAttachmentData: Codable {
|
|||||||
|
|
||||||
/// Helper for preferred text to display.
|
/// Helper for preferred text to display.
|
||||||
var displayText: String {
|
var displayText: String {
|
||||||
guard let displayName = displayName,
|
return items.map { $0.string }
|
||||||
displayName.count > 0 else {
|
.compactMap { $0 }
|
||||||
return matrixItemId
|
.joined(separator: " ")
|
||||||
}
|
|
||||||
|
|
||||||
return displayName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
/// Init.
|
/// Init.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - matrixItemId: Matrix item identifier (user id or room id)
|
/// - pillType: Type for the pill
|
||||||
/// - displayName: Matrix item display name (user or room display name)
|
/// - items: Items to display
|
||||||
/// - avatarUrl: Matrix item avatar URL (user or room avatar url)
|
|
||||||
/// - isHighlighted: Wether the pill should be highlighted
|
/// - isHighlighted: Wether the pill should be highlighted
|
||||||
/// - alpha: Alpha for pill display
|
/// - alpha: Alpha for pill display
|
||||||
/// - font: Font for the display name
|
/// - font: Font for the display name
|
||||||
init(matrixItemId: String,
|
init(pillType: PillType,
|
||||||
displayName: String?,
|
items: [PillTextAttachmentItem],
|
||||||
avatarUrl: String?,
|
|
||||||
isHighlighted: Bool,
|
isHighlighted: Bool,
|
||||||
alpha: CGFloat,
|
alpha: CGFloat,
|
||||||
font: UIFont) {
|
font: UIFont) {
|
||||||
self.matrixItemId = matrixItemId
|
self.pillType = pillType
|
||||||
self.displayName = displayName
|
self.items = items
|
||||||
self.avatarUrl = avatarUrl
|
|
||||||
self.isHighlighted = isHighlighted
|
self.isHighlighted = isHighlighted
|
||||||
self.alpha = alpha
|
self.alpha = alpha
|
||||||
self.font = font
|
self.font = font
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Codable
|
// MARK: - Codable
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case matrixItemId
|
case pillType
|
||||||
case displayName
|
case items
|
||||||
case avatarUrl
|
|
||||||
case isHighlighted
|
case isHighlighted
|
||||||
case alpha
|
case alpha
|
||||||
case font
|
case font
|
||||||
@@ -84,9 +116,8 @@ struct PillTextAttachmentData: Codable {
|
|||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
matrixItemId = try container.decode(String.self, forKey: .matrixItemId)
|
pillType = try container.decode(PillType.self, forKey: .pillType)
|
||||||
displayName = try? container.decode(String.self, forKey: .displayName)
|
items = try container.decode([PillTextAttachmentItem].self, forKey: .items)
|
||||||
avatarUrl = try? container.decode(String.self, forKey: .avatarUrl)
|
|
||||||
isHighlighted = try container.decode(Bool.self, forKey: .isHighlighted)
|
isHighlighted = try container.decode(Bool.self, forKey: .isHighlighted)
|
||||||
alpha = try container.decode(CGFloat.self, forKey: .alpha)
|
alpha = try container.decode(CGFloat.self, forKey: .alpha)
|
||||||
let fontData = try container.decode(Data.self, forKey: .font)
|
let fontData = try container.decode(Data.self, forKey: .font)
|
||||||
@@ -99,12 +130,36 @@ struct PillTextAttachmentData: Codable {
|
|||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(matrixItemId, forKey: .matrixItemId)
|
try container.encode(pillType, forKey: .pillType)
|
||||||
try? container.encode(displayName, forKey: .displayName)
|
try container.encode(items, forKey: .items)
|
||||||
try? container.encode(avatarUrl, forKey: .avatarUrl)
|
|
||||||
try container.encode(isHighlighted, forKey: .isHighlighted)
|
try container.encode(isHighlighted, forKey: .isHighlighted)
|
||||||
try container.encode(alpha, forKey: .alpha)
|
try container.encode(alpha, forKey: .alpha)
|
||||||
let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false)
|
let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false)
|
||||||
try container.encode(fontData, forKey: .font)
|
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 identifier
|
||||||
case markdown
|
case markdown
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Internal Methods
|
// MARK: - Internal Methods
|
||||||
/// Insert text attachments for pills inside given message attributed string.
|
/// Insert text attachments for pills inside given message attributed string.
|
||||||
///
|
///
|
||||||
@@ -52,17 +52,21 @@ class PillsFormatter: NSObject {
|
|||||||
roomState: MXRoomState,
|
roomState: MXRoomState,
|
||||||
andLatestRoomState latestRoomState: MXRoomState?,
|
andLatestRoomState latestRoomState: MXRoomState?,
|
||||||
isEditMode: Bool = false) -> NSAttributedString {
|
isEditMode: Bool = false) -> NSAttributedString {
|
||||||
|
|
||||||
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
||||||
newAttr.vc_enumerateAttribute(.link) { (url: URL, range: NSRange, _) in
|
newAttr.vc_enumerateAttribute(.link) { (url: URL, range: NSRange, _) in
|
||||||
if let userId = userIdFromPermalink(url.absoluteString),
|
|
||||||
let roomMember = roomMember(withUserId: userId,
|
let provider = PillProvider(withSession: session,
|
||||||
roomState: roomState,
|
eventFormatter: eventFormatter,
|
||||||
andLatestRoomState: latestRoomState) {
|
event: event,
|
||||||
let isHighlighted = roomMember.userId == session.myUserId && event.sender != session.myUserId
|
roomState: roomState,
|
||||||
let attachmentString = mentionPill(withRoomMember: roomMember,
|
andLatestRoomState: latestRoomState,
|
||||||
andUrl: isEditMode ? nil : url,
|
isEditMode: isEditMode)
|
||||||
isHighlighted: isHighlighted,
|
|
||||||
font: eventFormatter.defaultTextFont)
|
// 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)
|
newAttr.replaceCharacters(in: range, with: attachmentString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,25 +84,27 @@ class PillsFormatter: NSObject {
|
|||||||
mode: PillsReplacementTextMode = .displayname) -> String {
|
mode: PillsReplacementTextMode = .displayname) -> String {
|
||||||
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
||||||
newAttr.vc_enumerateAttribute(.attachment) { (attachment: PillTextAttachment, range: NSRange, _) in
|
newAttr.vc_enumerateAttribute(.attachment) { (attachment: PillTextAttachment, range: NSRange, _) in
|
||||||
if let displayText = attachment.data?.displayText,
|
guard let data = attachment.data else {
|
||||||
let userId = attachment.data?.matrixItemId,
|
return
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return newAttr.string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Creates an attributed string containing a pill for given room member.
|
/// Creates an attributed string containing a pill for given room member.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -111,17 +117,13 @@ class PillsFormatter: NSObject {
|
|||||||
andUrl url: URL? = nil,
|
andUrl url: URL? = nil,
|
||||||
isHighlighted: Bool,
|
isHighlighted: Bool,
|
||||||
font: UIFont) -> NSAttributedString {
|
font: UIFont) -> NSAttributedString {
|
||||||
|
|
||||||
guard let attachment = PillTextAttachment(withRoomMember: roomMember, isHighlighted: isHighlighted, font: font) else {
|
guard let attachment = PillTextAttachment(withRoomMember: roomMember, isHighlighted: isHighlighted, font: font) else {
|
||||||
return NSAttributedString(string: roomMember.displayname)
|
return NSAttributedString(string: roomMember.displayname)
|
||||||
}
|
}
|
||||||
let string = NSMutableAttributedString(attachment: attachment)
|
return attributedStringWithAttachment(attachment, link: url, font: font)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update alpha of all `PillTextAttachment` contained in given attributed string.
|
/// Update alpha of all `PillTextAttachment` contained in given attributed string.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -140,43 +142,37 @@ class PillsFormatter: NSObject {
|
|||||||
/// - roomState: room state for refresh, should be the latest available
|
/// - roomState: room state for refresh, should be the latest available
|
||||||
static func refreshPills(in attributedString: NSAttributedString, with roomState: MXRoomState) {
|
static func refreshPills(in attributedString: NSAttributedString, with roomState: MXRoomState) {
|
||||||
attributedString.vc_enumerateAttribute(.attachment) { (pill: PillTextAttachment, range: NSRange, _) in
|
attributedString.vc_enumerateAttribute(.attachment) { (pill: PillTextAttachment, range: NSRange, _) in
|
||||||
guard let userId = pill.data?.matrixItemId,
|
|
||||||
let roomMember = roomState.members.member(withUserId: userId) else {
|
switch pill.data?.pillType {
|
||||||
return
|
case .user(let userId):
|
||||||
}
|
guard let roomMember = roomState.members.member(withUserId: userId) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
pill.data?.displayName = roomMember.displayname
|
pill.data?.items = [
|
||||||
pill.data?.avatarUrl = roomMember.avatarUrl
|
.avatar(url: roomMember.avatarUrl,
|
||||||
|
string: roomMember.displayname,
|
||||||
|
matrixId: roomMember.userId),
|
||||||
|
.text(roomMember.displayname)
|
||||||
|
]
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
@available (iOS 15.0, *)
|
@available (iOS 15.0, *)
|
||||||
private extension PillsFormatter {
|
extension PillsFormatter {
|
||||||
/// Extract user id from given permalink
|
|
||||||
/// - Parameter permalink: the permalink
|
static func attributedStringWithAttachment(_ attachment: PillTextAttachment, link: URL?, font: UIFont) -> NSAttributedString {
|
||||||
/// - Returns: userId, if any
|
let string = NSMutableAttributedString(attachment: attachment)
|
||||||
static func userIdFromPermalink(_ permalink: String) -> String? {
|
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
|
||||||
let baseUrl: String
|
if let url = link {
|
||||||
if let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl {
|
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
|
||||||
baseUrl = String(format: "%@/#/user/", clientBaseUrl)
|
|
||||||
} else {
|
|
||||||
baseUrl = String(format: "%@/#/", kMXMatrixDotToUrl)
|
|
||||||
}
|
}
|
||||||
return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil
|
return string
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ final class SpaceAvatarView: AvatarView, NibOwnerLoadable {
|
|||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.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
|
// MARK: - Public
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ final class UserAvatarView: AvatarView {
|
|||||||
|
|
||||||
private func commonInit() {
|
private func commonInit() {
|
||||||
let avatarImageView = MXKImageView()
|
let avatarImageView = MXKImageView()
|
||||||
|
avatarImageView.frame = self.frame
|
||||||
self.vc_addSubViewMatchingParent(avatarImageView)
|
self.vc_addSubViewMatchingParent(avatarImageView)
|
||||||
self.avatarImageView = avatarImageView
|
self.avatarImageView = avatarImageView
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,7 +423,7 @@
|
|||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
XCTAssertEqual(hasLink, false, @"There should be no link in this case. We let the UI manage the link");
|
XCTAssertEqual(hasLink, true, @"There should be a link, so that a Pill can be rendered for this permalink.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Event sender/target info
|
#pragma mark - Event sender/target info
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
//
|
||||||
|
// 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 XCTest
|
||||||
|
@testable import Element
|
||||||
|
|
||||||
|
@available (iOS 15.0, *)
|
||||||
|
final class PillTypeTests: XCTestCase {
|
||||||
|
|
||||||
|
func testUserPill() throws {
|
||||||
|
let urls = [
|
||||||
|
"https://matrix.to/#/@bob:matrix.org",
|
||||||
|
"https://matrix.to/#/user/@bob:matrix.org"
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
switch PillType.from(url: URL(string: url)!) {
|
||||||
|
case .user(let userId):
|
||||||
|
XCTAssertEqual(userId, "@bob:matrix.org")
|
||||||
|
default:
|
||||||
|
XCTFail("Should be a .user pill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoomPill() throws {
|
||||||
|
let urls = [
|
||||||
|
"https://matrix.to/#/!JppIaYcVkyCiSBVzBn:localhost",
|
||||||
|
"https://matrix.to/#/!JppIaYcVkyCiSBVzBn:localhost?via=localhost",
|
||||||
|
"https://matrix.to/#/room/!JppIaYcVkyCiSBVzBn:localhost"
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
switch PillType.from(url: URL(string: url)!) {
|
||||||
|
case .room(let roomId):
|
||||||
|
XCTAssertEqual(roomId, "!JppIaYcVkyCiSBVzBn:localhost")
|
||||||
|
default:
|
||||||
|
XCTFail("Should be a .room pill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoomAlias() throws {
|
||||||
|
let urls = [
|
||||||
|
"https://matrix.to/#/%23room-alias:localhost",
|
||||||
|
"https://matrix.to/#/room/%23room-alias:localhost"
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
switch PillType.from(url: URL(string: url)!) {
|
||||||
|
case .room(let roomId):
|
||||||
|
XCTAssertEqual(roomId, "#room-alias:localhost")
|
||||||
|
default:
|
||||||
|
XCTFail("Should be a .room pill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMessagePill() throws {
|
||||||
|
let urls = [
|
||||||
|
"https://matrix.to/#/!JppIaYcVkyCiSBVzBn:localhost/$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc",
|
||||||
|
"https://matrix.to/#/!JppIaYcVkyCiSBVzBn:localhost/$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc?via=localhost"
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
switch PillType.from(url: URL(string: url)!) {
|
||||||
|
case .message(let roomId, let eventId):
|
||||||
|
XCTAssertEqual(roomId, "!JppIaYcVkyCiSBVzBn:localhost")
|
||||||
|
XCTAssertEqual(eventId, "$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc")
|
||||||
|
default:
|
||||||
|
XCTFail("Should be a .message pill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMessagePillWithRoomAlias() throws {
|
||||||
|
let urls = [
|
||||||
|
"https://matrix.to/#/%23room-alias:localhost/$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc?via=localhost"
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
switch PillType.from(url: URL(string: url)!) {
|
||||||
|
case .message(let roomId, let eventId):
|
||||||
|
XCTAssertEqual(roomId, "#room-alias:localhost")
|
||||||
|
XCTAssertEqual(eventId, "$4uvJnQsShl_2OqfqO4dkmUq-mKula7HUx-ictOTPmPc")
|
||||||
|
default:
|
||||||
|
XCTFail("Should be a .message pill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNotAPermalink() throws {
|
||||||
|
XCTAssertNil(PillType.from(url: URL(string: "matrix.org")!))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -27,10 +27,42 @@ private enum Inputs {
|
|||||||
static let aliceNewAvatarUrl = "mxc://matrix.org/VyNYAgaFdlLojoOeZETtQ"
|
static let aliceNewAvatarUrl = "mxc://matrix.org/VyNYAgaFdlLojoOeZETtQ"
|
||||||
static let aliceMember = FakeMXRoomMember(displayname: aliceDisplayname, avatarUrl: aliceAvatarUrl, userId: aliceUserId)
|
static let aliceMember = FakeMXRoomMember(displayname: aliceDisplayname, avatarUrl: aliceAvatarUrl, userId: aliceUserId)
|
||||||
static let aliceMemberAway = FakeMXRoomMember(displayname: aliceAwayDisplayname, avatarUrl: aliceNewAvatarUrl, userId: "@alice:matrix.org")
|
static let aliceMemberAway = FakeMXRoomMember(displayname: aliceAwayDisplayname, avatarUrl: aliceNewAvatarUrl, userId: "@alice:matrix.org")
|
||||||
static let bobMember = FakeMXRoomMember(displayname: "Bob", avatarUrl: "", userId: "@bob:matrix.org")
|
|
||||||
static let alicePermalink = "https://matrix.to/#/@alice:matrix.org"
|
static let alicePermalink = "https://matrix.to/#/@alice:matrix.org"
|
||||||
static let mentionToAlice = NSAttributedString(string: aliceDisplayname, attributes: [.link: URL(string: alicePermalink)!])
|
static let mentionToAlice = NSAttributedString(string: aliceDisplayname, attributes: [.link: URL(string: alicePermalink)!])
|
||||||
static let markdownLinkToAlice = "[Alice](\(alicePermalink))"
|
static let markdownLinkToAlice = "[Alice](\(alicePermalink))"
|
||||||
|
|
||||||
|
static let bobUserId = "@bob:matrix.org"
|
||||||
|
static let bobDisplayname = "Bob"
|
||||||
|
static let bobAvatarUrl = "mxc://matrix.org/VyNYBgahazAzUuOeZETtQ"
|
||||||
|
static let bobMember = FakeMXRoomMember(displayname: bobDisplayname, avatarUrl: bobAvatarUrl, userId: bobUserId)
|
||||||
|
|
||||||
|
static let anotherUserId = "@another.user:matrix.org"
|
||||||
|
static let anotherUserPermalink = "https://matrix.to/#/@another.user:matrix.org"
|
||||||
|
static let markdownLinkToAnotherUser = "[Another user](\(alicePermalink))"
|
||||||
|
static let mentionToAnotherUser = NSAttributedString(string: anotherUserPermalink, attributes: [.link: URL(string: anotherUserPermalink)!])
|
||||||
|
static let mentionToAnotherUserWithLabel = NSAttributedString(string: "Link text", attributes: [.link: URL(string: anotherUserPermalink)!])
|
||||||
|
|
||||||
|
static let roomId = "!vWieJcXcUdMwavNSvy:matrix.org"
|
||||||
|
static let roomAlias = "#fake_room_alias:matrix.org"
|
||||||
|
static let roomDisplayName = "Sample Room"
|
||||||
|
static let roomPermalink = "https://matrix.to/#/\(roomId)"
|
||||||
|
static let roomAliasPermalink = "https://matrix.to/%23/\(roomAlias)"
|
||||||
|
static let roomAvatarUrl = "mxc://matrix.org/VzNZAgahaiAzUoOeZETtQ"
|
||||||
|
static let mentionToRoom = NSAttributedString(string: roomPermalink, attributes: [.link: URL(string: roomPermalink)!])
|
||||||
|
static let mentionToRoomWithLabel = NSAttributedString(string: roomDisplayName, attributes: [.link: URL(string: roomPermalink)!])
|
||||||
|
static let mentionToRoomAlias = NSAttributedString(string: roomDisplayName, attributes: [.link: URL(string: roomAliasPermalink)!])
|
||||||
|
|
||||||
|
static let anotherRoomId = "!zWieBcUcUdMwavNSvy:matrix.org"
|
||||||
|
static let anotherRoomDisplayName = "Room/Space"
|
||||||
|
static let anotherRoomAvatarUrl = "mxc://matrix.org/VzNZBgajauAzUoOeZETtQ"
|
||||||
|
|
||||||
|
static let messageEventId = "$JrEsoQO77MCdAubG6z-5oXlOBy1I5QL9FTut_Giztoc"
|
||||||
|
static let messagePermalink = "https://matrix.to/#/\(roomId)/\(messageEventId)?via=matrix.org"
|
||||||
|
static let messageAnotherRoomPermalink = "https://matrix.to/#/\(anotherRoomId)/\(messageEventId)?via=matrix.org"
|
||||||
|
|
||||||
|
static let pillAnotherUserWithLinkText = "Link text"
|
||||||
|
static let pillMessageAnotherRoomText = "Message in Sample Room"
|
||||||
|
static let pillMessageFromBobText = "Message from Bob"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Tests
|
// MARK: - Tests
|
||||||
@@ -47,11 +79,24 @@ class PillsFormatterTests: XCTestCase {
|
|||||||
// Attachment has correct type.
|
// Attachment has correct type.
|
||||||
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
// Pill data contains Alice's displayname and avatar url.
|
// Pill data contains Alice's displayname and avatar url.
|
||||||
XCTAssertEqual(pillTextAttachment?.data?.displayText, Inputs.aliceDisplayname)
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
XCTAssertEqual(pillTextAttachment?.data?.avatarUrl, Inputs.aliceAvatarUrl)
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceDisplayname)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .user(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.aliceUserId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .avatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.aliceAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the avatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .user")
|
||||||
|
}
|
||||||
|
|
||||||
// Pill has expected size.
|
// Pill has expected size.
|
||||||
let expectedSize = PillAttachmentViewProvider.size(forDisplayText: pillTextAttachment!.data!.displayText,
|
let expectedSize = pillTextAttachment?.size(forFont: pillTextAttachment!.data!.font)
|
||||||
andFont: pillTextAttachment!.data!.font)
|
|
||||||
XCTAssertEqual(pillTextAttachment?.bounds.size, expectedSize)
|
XCTAssertEqual(pillTextAttachment?.bounds.size, expectedSize)
|
||||||
|
|
||||||
PillsFormatter.refreshPills(in: messageWithPills,
|
PillsFormatter.refreshPills(in: messageWithPills,
|
||||||
@@ -60,11 +105,23 @@ class PillsFormatterTests: XCTestCase {
|
|||||||
// Alice's pill is still highlighted.
|
// Alice's pill is still highlighted.
|
||||||
XCTAssert(pillTextAttachment?.data?.isHighlighted == true)
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == true)
|
||||||
// Pill data is refreshed with correct data.
|
// Pill data is refreshed with correct data.
|
||||||
XCTAssertEqual(refreshedPillTextAttachment?.data?.displayText, Inputs.aliceAwayDisplayname)
|
let updatedPillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
XCTAssertEqual(refreshedPillTextAttachment?.data?.avatarUrl, Inputs.aliceNewAvatarUrl)
|
XCTAssertEqual(updatedPillTextAttachmentData.displayText, Inputs.aliceAwayDisplayname)
|
||||||
|
switch updatedPillTextAttachmentData.pillType {
|
||||||
|
case .user(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.aliceUserId)
|
||||||
|
switch updatedPillTextAttachmentData.items.first {
|
||||||
|
case .avatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.aliceNewAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the avatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .user")
|
||||||
|
}
|
||||||
|
|
||||||
// Pill size is updated
|
// Pill size is updated
|
||||||
let newExpectedSize = PillAttachmentViewProvider.size(forDisplayText: refreshedPillTextAttachment!.data!.displayText,
|
let newExpectedSize = pillTextAttachment?.size(forFont: refreshedPillTextAttachment!.data!.font)
|
||||||
andFont: refreshedPillTextAttachment!.data!.font)
|
|
||||||
XCTAssertEqual(refreshedPillTextAttachment?.bounds.size, newExpectedSize)
|
XCTAssertEqual(refreshedPillTextAttachment?.bounds.size, newExpectedSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +129,21 @@ class PillsFormatterTests: XCTestCase {
|
|||||||
let messageWithPills = createMessageWithMentionFromBobToAliceWithLatestRoomState()
|
let messageWithPills = createMessageWithMentionFromBobToAliceWithLatestRoomState()
|
||||||
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
// Pill uses the latest room state data.
|
// Pill uses the latest room state data.
|
||||||
XCTAssertEqual(pillTextAttachment?.data?.displayText, Inputs.aliceAwayDisplayname)
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
XCTAssertEqual(pillTextAttachment?.data?.avatarUrl, Inputs.aliceNewAvatarUrl)
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceAwayDisplayname)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .user(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.aliceUserId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .avatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.aliceNewAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the avatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .message")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPillsToMarkdown() {
|
func testPillsToMarkdown() {
|
||||||
@@ -89,6 +159,292 @@ class PillsFormatterTests: XCTestCase {
|
|||||||
XCTAssertEqual(messageWithDisplayname, Inputs.messageStart + Inputs.aliceDisplayname)
|
XCTAssertEqual(messageWithDisplayname, Inputs.messageStart + Inputs.aliceDisplayname)
|
||||||
XCTAssertEqual(messageWithUserId, Inputs.messageStart + Inputs.aliceUserId)
|
XCTAssertEqual(messageWithUserId, Inputs.messageStart + Inputs.aliceUserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to an unknown user (not a room member)
|
||||||
|
func testPillMentionningRoomMember() {
|
||||||
|
let messageWithPills = createMessageWithMentionFromBobToAlice()
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill uses the latest room state data.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.aliceDisplayname)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .user(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.aliceUserId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .avatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.aliceAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the avatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to an unknown user (not a room member)
|
||||||
|
func testPillMentionningUnknownUser() {
|
||||||
|
let messageWithPills = createMessageWithMentionFromBobToAnotherUser()
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill uses the latest room state data.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.anotherUserId)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .user(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.anotherUserId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .asset(let name, _):
|
||||||
|
XCTAssertEqual(name, "pill_user")
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the asset")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to an unknown user (not a room member) with a formatted text (HTML or MARKDOWN)
|
||||||
|
// In this case, we don't want to pillify the link
|
||||||
|
func testPillMentionningUnknownUserWithFormattedText() {
|
||||||
|
let messageWithPills = createMessageWithMentionFromBobToAnotherUser(withLinkText: true)
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
XCTAssertNil(pillTextAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to a room
|
||||||
|
func testPillMentionningRoom() {
|
||||||
|
let messageWithPills = createMessageWithMentionToRoom()
|
||||||
|
XCTAssertEqual(messageWithPills.length, Inputs.messageStart.count + 1) // +1 non-unicode character for the pill/textAttachment
|
||||||
|
XCTAssert(messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) is PillTextAttachment)
|
||||||
|
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill is not highlighted.
|
||||||
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
|
||||||
|
// Attachment has correct type.
|
||||||
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
|
// Pill data contains the correct displayname and avatar url.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .room(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.roomId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .avatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.roomAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the avatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to a space
|
||||||
|
func testPillMentionningSpace() {
|
||||||
|
let messageWithPills = createMessageWithMentionToRoom(isSpace: true)
|
||||||
|
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill is not highlighted.
|
||||||
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
|
||||||
|
// Attachment has correct type.
|
||||||
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
|
// Pill data contains the correct displayname and avatar url.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .room(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.roomId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .spaceAvatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.roomAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the spaceAvatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to a room alias
|
||||||
|
func testPillMentionningRoomByAlias() {
|
||||||
|
let messageWithPills = createMessageWithMentionToRoom(usingAlias: true)
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill is not highlighted.
|
||||||
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
|
||||||
|
// Attachment has correct type.
|
||||||
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
|
// Pill data contains the correct displayname and avatar url.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.roomDisplayName)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .room(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.roomAlias)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .avatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.roomAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the avatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to an unknown room
|
||||||
|
func testPillMentionningUnknownRoom() {
|
||||||
|
let messageWithPills = createMessageWithMentionToRoom(knownRoom: false)
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill is not highlighted.
|
||||||
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
|
||||||
|
// Attachment has correct type.
|
||||||
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
|
// Pill data contains the correct displayname and avatar url.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillRoomFallbackDisplayName)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .room(let userId):
|
||||||
|
XCTAssertEqual(userId, Inputs.roomId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .asset(let assetName, let parameters):
|
||||||
|
XCTAssertEqual(assetName, "link_icon")
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the asset")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to an unknown room using a formatted text (HTML or MARKDOWN)
|
||||||
|
func testPillMentionningUnknownRoomWithFormattedText() {
|
||||||
|
let messageWithPills = createMessageWithMentionToRoom(knownRoom: false, withLinkText: "Link label")
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
XCTAssertNil(pillTextAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to a message using a formatted text (HTML or MARKDOWN)
|
||||||
|
func testPillMentionningMessageWithLabel() {
|
||||||
|
let messageWithPills = createMessageWithMentionToMessage(from: Inputs.bobMember, withLabel: "Link label")
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
XCTAssertNil(pillTextAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to a message sent by a room member in the current room
|
||||||
|
func testPillMentionningMessageInCurrentRoomFromRoomMember() {
|
||||||
|
// Test: a mention to current room message, sent by a room member (Bob)
|
||||||
|
let messageWithPills = createMessageWithMentionToMessage(from: Inputs.bobMember, withLabel: Inputs.messagePermalink)
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill is not highlighted.
|
||||||
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
|
||||||
|
// Attachment has correct type.
|
||||||
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
|
// Pill data contains the correct displayname and avatar url.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, Inputs.pillMessageFromBobText)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .message(let roomId, let messageId):
|
||||||
|
XCTAssertEqual(roomId, Inputs.roomId)
|
||||||
|
XCTAssertEqual(messageId, Inputs.messageEventId)
|
||||||
|
let firstItem = pillTextAttachmentData.items[0]
|
||||||
|
switch firstItem {
|
||||||
|
case .avatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.bobAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the avatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to a message sent in the current room from an unknown user
|
||||||
|
func testPillMentionningMessageInCurrentRoomFromUnknownUser() {
|
||||||
|
let messageWithPills = createMessageWithMentionToMessage(sentBy: Inputs.anotherUserId, withLabel: Inputs.messagePermalink)
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill is not highlighted.
|
||||||
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
|
||||||
|
// Attachment has correct type.
|
||||||
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
|
// Pill data contains the correct displayname and avatar url.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessage)
|
||||||
|
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .message(let roomId, let messageId):
|
||||||
|
XCTAssertEqual(roomId, Inputs.roomId)
|
||||||
|
XCTAssertEqual(messageId, Inputs.messageEventId)
|
||||||
|
let firstItem = pillTextAttachmentData.items[0]
|
||||||
|
switch firstItem {
|
||||||
|
case .asset(let name, _):
|
||||||
|
XCTAssertEqual(name, "link_icon")
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the asset")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to a message in another room
|
||||||
|
func testPillMentionningMessageInAnotherRoom() {
|
||||||
|
let messageWithPills = createMessageWithMentionToAnotherRoomMessage(knownRoom: true, withLabel: Inputs.messageAnotherRoomPermalink)
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill is not highlighted.
|
||||||
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
|
||||||
|
// Attachment has correct type.
|
||||||
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
|
// Pill data contains the correct displayname and avatar url.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessageIn(Inputs.anotherRoomDisplayName))
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .message(let roomId, let messageId):
|
||||||
|
XCTAssertEqual(roomId, Inputs.anotherRoomId)
|
||||||
|
XCTAssertEqual(messageId, Inputs.messageEventId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .avatar(let url, _, _):
|
||||||
|
XCTAssertEqual(url, Inputs.anotherRoomAvatarUrl)
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the avatar")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case: a mention to a message in an unknown room
|
||||||
|
func testPillMentionningMessageInUnknownRoom() {
|
||||||
|
let messageWithPills = createMessageWithMentionToAnotherRoomMessage(knownRoom: false, withLabel: Inputs.messageAnotherRoomPermalink)
|
||||||
|
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||||
|
// Pill is not highlighted.
|
||||||
|
XCTAssert(pillTextAttachment?.data?.isHighlighted == false)
|
||||||
|
// Attachment has correct type.
|
||||||
|
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||||
|
// Pill data contains the correct displayname and avatar url.
|
||||||
|
XCTAssertNotNil(pillTextAttachment?.data)
|
||||||
|
let pillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||||
|
XCTAssertEqual(pillTextAttachmentData.displayText, VectorL10n.pillMessage)
|
||||||
|
switch pillTextAttachmentData.pillType {
|
||||||
|
case .message(let roomId, let messageId):
|
||||||
|
XCTAssertEqual(roomId, Inputs.anotherRoomId)
|
||||||
|
XCTAssertEqual(messageId, Inputs.messageEventId)
|
||||||
|
switch pillTextAttachmentData.items.first {
|
||||||
|
case .asset(let name, let parameters):
|
||||||
|
XCTAssertEqual(name, "link_icon")
|
||||||
|
default:
|
||||||
|
XCTFail("First pill item should be the asset")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
XCTFail("Pill should be of type .message")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 15.0, *)
|
@available(iOS 15.0, *)
|
||||||
@@ -105,6 +461,24 @@ private extension PillsFormatterTests {
|
|||||||
andLatestRoomState: nil)
|
andLatestRoomState: nil)
|
||||||
return messageWithPills
|
return messageWithPills
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createMessageWithMentionFromBobToAnotherUser(withLinkText: Bool = false) -> NSAttributedString {
|
||||||
|
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
|
||||||
|
if withLinkText {
|
||||||
|
formattedMessage.append(Inputs.mentionToAnotherUserWithLabel)
|
||||||
|
} else {
|
||||||
|
formattedMessage.append(Inputs.mentionToAnotherUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
|
||||||
|
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
|
||||||
|
withSession: session,
|
||||||
|
eventFormatter: EventFormatter(matrixSession: session),
|
||||||
|
event: FakeMXEvent(sender: Inputs.anotherUserId),
|
||||||
|
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()),
|
||||||
|
andLatestRoomState: nil)
|
||||||
|
return messageWithPills
|
||||||
|
}
|
||||||
|
|
||||||
func createMessageWithMentionFromBobToAliceWithLatestRoomState() -> NSAttributedString {
|
func createMessageWithMentionFromBobToAliceWithLatestRoomState() -> NSAttributedString {
|
||||||
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
|
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
|
||||||
@@ -118,35 +492,269 @@ private extension PillsFormatterTests {
|
|||||||
andLatestRoomState: FakeMXRoomState(roomMembers: FakeMXUpdatedRoomMembers()))
|
andLatestRoomState: FakeMXRoomState(roomMembers: FakeMXUpdatedRoomMembers()))
|
||||||
return messageWithPills
|
return messageWithPills
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createMessageWithMentionToRoom(isSpace: Bool = false, knownRoom: Bool = true, usingAlias: Bool = false, withLinkText: String? = nil) -> NSAttributedString {
|
||||||
|
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
|
||||||
|
let mention: NSAttributedString
|
||||||
|
if usingAlias {
|
||||||
|
mention = NSAttributedString(string: withLinkText ?? Inputs.roomAliasPermalink , attributes: [.link: URL(string: Inputs.roomAliasPermalink)!])
|
||||||
|
} else {
|
||||||
|
mention = NSAttributedString(string: withLinkText ?? Inputs.roomPermalink , attributes: [.link: URL(string: Inputs.roomPermalink)!])
|
||||||
|
}
|
||||||
|
formattedMessage.append(mention)
|
||||||
|
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
|
||||||
|
let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: Inputs.bobMember.userId)
|
||||||
|
session.store = FakeMXStore(withEvents: [event])
|
||||||
|
if knownRoom {
|
||||||
|
let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)!
|
||||||
|
let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId,
|
||||||
|
displayName: Inputs.roomDisplayName,
|
||||||
|
alias: Inputs.roomAlias,
|
||||||
|
avatar: Inputs.roomAvatarUrl,
|
||||||
|
matrixSession: session)
|
||||||
|
if isSpace {
|
||||||
|
roomSummary.roomType = .space
|
||||||
|
}
|
||||||
|
session.addFakeRoom(room)
|
||||||
|
session.addFakeRoomSummary(roomSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
|
||||||
|
withSession: session,
|
||||||
|
eventFormatter: EventFormatter(matrixSession: session),
|
||||||
|
event: event,
|
||||||
|
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers()),
|
||||||
|
andLatestRoomState: nil)
|
||||||
|
return messageWithPills
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMessageWithMentionToMessage(from sender: MXRoomMember, withLabel string: String) -> NSAttributedString {
|
||||||
|
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
|
||||||
|
formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messagePermalink)!]))
|
||||||
|
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
|
||||||
|
let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: sender.userId)
|
||||||
|
session.store = FakeMXStore(withEvents: [event])
|
||||||
|
let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)!
|
||||||
|
let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId,
|
||||||
|
displayName: Inputs.roomDisplayName,
|
||||||
|
alias: Inputs.roomAlias,
|
||||||
|
avatar: Inputs.roomAvatarUrl,
|
||||||
|
matrixSession: session)
|
||||||
|
session.addFakeRoom(room)
|
||||||
|
session.addFakeRoomSummary(roomSummary)
|
||||||
|
|
||||||
|
|
||||||
|
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
|
||||||
|
withSession: session,
|
||||||
|
eventFormatter: EventFormatter(matrixSession: session),
|
||||||
|
event: event,
|
||||||
|
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId),
|
||||||
|
andLatestRoomState: nil)
|
||||||
|
return messageWithPills
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMessageWithMentionToMessage(sentBy senderId: String, withLabel string: String) -> NSAttributedString {
|
||||||
|
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
|
||||||
|
formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messagePermalink)!]))
|
||||||
|
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
|
||||||
|
let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: senderId)
|
||||||
|
session.store = FakeMXStore(withEvents: [event])
|
||||||
|
let room = FakeMXRoom(roomId: Inputs.roomId, matrixSession: session, andStore: nil)!
|
||||||
|
let roomSummary = FakeMXRoomSummary(roomId: Inputs.roomId,
|
||||||
|
displayName: Inputs.roomDisplayName,
|
||||||
|
alias: Inputs.roomAlias,
|
||||||
|
avatar: Inputs.roomAvatarUrl,
|
||||||
|
matrixSession: session)
|
||||||
|
session.addFakeRoom(room)
|
||||||
|
session.addFakeRoomSummary(roomSummary)
|
||||||
|
|
||||||
|
|
||||||
|
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
|
||||||
|
withSession: session,
|
||||||
|
eventFormatter: EventFormatter(matrixSession: session),
|
||||||
|
event: event,
|
||||||
|
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId),
|
||||||
|
andLatestRoomState: nil)
|
||||||
|
return messageWithPills
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMessageWithMentionToAnotherRoomMessage(knownRoom: Bool, withLabel string: String) -> NSAttributedString {
|
||||||
|
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
|
||||||
|
formattedMessage.append(NSAttributedString(string: string, attributes: [.link: URL(string: Inputs.messageAnotherRoomPermalink)!]))
|
||||||
|
let session = FakeMXSession(myUserId: Inputs.aliceMember.userId)
|
||||||
|
let event = FakeMXEvent(eventId: Inputs.messageEventId, sender: Inputs.anotherUserId)
|
||||||
|
session.store = FakeMXStore(withEvents: [event])
|
||||||
|
if knownRoom {
|
||||||
|
let room = FakeMXRoom(roomId: Inputs.anotherRoomId, matrixSession: session, andStore: nil)!
|
||||||
|
let roomSummary = FakeMXRoomSummary(roomId: Inputs.anotherRoomId,
|
||||||
|
displayName: Inputs.anotherRoomDisplayName,
|
||||||
|
alias: nil,
|
||||||
|
avatar: Inputs.anotherRoomAvatarUrl,
|
||||||
|
matrixSession: session)
|
||||||
|
session.addFakeRoom(room)
|
||||||
|
session.addFakeRoomSummary(roomSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageWithPills = PillsFormatter.insertPills(in: formattedMessage,
|
||||||
|
withSession: session,
|
||||||
|
eventFormatter: EventFormatter(matrixSession: session),
|
||||||
|
event: event,
|
||||||
|
roomState: FakeMXRoomState(roomMembers: FakeMXRoomMembers(), roomId: Inputs.roomId),
|
||||||
|
andLatestRoomState: nil)
|
||||||
|
return messageWithPills
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Mock objects
|
// MARK: - Mock objects
|
||||||
private class FakeMXSession: MXSession {
|
private class FakeMXSession: MXSession {
|
||||||
private var mockMyUserId: String
|
private var mockMyUserId: String
|
||||||
|
private var mockRooms: [FakeMXRoom] = []
|
||||||
|
private var mockRoomSummaries: [String: FakeMXRoomSummary] = [:]
|
||||||
|
private var mockStore: FakeMXStore?
|
||||||
|
|
||||||
init(myUserId: String) {
|
init(myUserId: String) {
|
||||||
mockMyUserId = myUserId
|
mockMyUserId = myUserId
|
||||||
|
let credentials = MXCredentials(homeServer: "mock_home_server",
|
||||||
super.init()
|
userId: "mock_user_id",
|
||||||
|
accessToken: "mock_access_token")
|
||||||
|
let client = MXRestClient(credentials: credentials)
|
||||||
|
super.init(matrixRestClient: client)
|
||||||
}
|
}
|
||||||
|
|
||||||
override var myUserId: String! {
|
override var myUserId: String! {
|
||||||
return mockMyUserId
|
return mockMyUserId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addFakeRoom(_ room: FakeMXRoom) {
|
||||||
|
mockRooms.append(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func room(withRoomId roomId: String!) -> MXRoom! {
|
||||||
|
return mockRooms.first(where: { $0.roomId == roomId })
|
||||||
|
}
|
||||||
|
|
||||||
|
override func room(withAlias roomAlias: String) -> MXRoom? {
|
||||||
|
for (roomId, summary) in mockRoomSummaries {
|
||||||
|
if summary.aliases.contains(roomAlias) {
|
||||||
|
return room(withRoomId: roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override func roomSummary(withRoomId roomId: String!) -> MXRoomSummary? {
|
||||||
|
return mockRoomSummaries[roomId]
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFakeRoomSummary(_ roomSummary: FakeMXRoomSummary) {
|
||||||
|
self.mockRoomSummaries[roomSummary.roomId] = roomSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
override var store: MXStore! {
|
||||||
|
get { return mockStore }
|
||||||
|
set { mockStore = newValue as? FakeMXStore }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeMXStore: MXMemoryStore {
|
||||||
|
private var mockEvents: [MXEvent]
|
||||||
|
|
||||||
|
init(withEvents events: [MXEvent]) {
|
||||||
|
self.mockEvents = events
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func event(withEventId eventId: String, inRoom roomId: String) -> MXEvent? {
|
||||||
|
return mockEvents.first(where: { $0.eventId == eventId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeMXRoom: MXRoom {
|
||||||
|
private var mockDisplayName: String? = nil
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init!(roomId: String!, matrixSession mxSession: MXSession!, andStore store: MXStore!) {
|
||||||
|
super.init(roomId: roomId, matrixSession: mxSession, andStore: store)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var summary: MXRoomSummary! {
|
||||||
|
return mxSession?.roomSummary(withRoomId: self.roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeMXRoomSummary: MXRoomSummary {
|
||||||
|
private var mockDisplayName: String?
|
||||||
|
private var mockAliases: [String]?
|
||||||
|
private var mockAvatar: String? = nil
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(roomId: String, displayName: String, alias: String?, avatar: String?, matrixSession mxSession: MXSession) {
|
||||||
|
super.init(roomId: roomId, andMatrixSession: mxSession)
|
||||||
|
self.mockDisplayName = displayName
|
||||||
|
self.mockAliases = alias.flatMap { [$0] } ?? []
|
||||||
|
self.mockAvatar = avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
override init!(roomId: String!, matrixSession mxSession: MXSession!, andStore store: MXStore!) {
|
||||||
|
super.init(roomId: roomId, matrixSession: mxSession, andStore: store)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init!(roomId: String!, andMatrixSession mxSession: MXSession!) {
|
||||||
|
super.init(roomId: roomId, andMatrixSession: mxSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var displayName: String! {
|
||||||
|
get { return mockDisplayName }
|
||||||
|
set { mockDisplayName = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
override var avatar: String! {
|
||||||
|
get { return mockAvatar }
|
||||||
|
set { mockAvatar = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
override var aliases: [String]! {
|
||||||
|
get { return mockAliases }
|
||||||
|
set { mockAliases = newValue }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeMXRoomState: MXRoomState {
|
private class FakeMXRoomState: MXRoomState {
|
||||||
private let mockRoomMembers: MXRoomMembers
|
private let mockRoomMembers: MXRoomMembers
|
||||||
|
private let mockRoomId: String?
|
||||||
|
|
||||||
init(roomMembers: MXRoomMembers) {
|
init(roomMembers: MXRoomMembers) {
|
||||||
mockRoomMembers = roomMembers
|
mockRoomMembers = roomMembers
|
||||||
|
mockRoomId = nil
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(roomMembers: MXRoomMembers, roomId: String) {
|
||||||
|
mockRoomMembers = roomMembers
|
||||||
|
mockRoomId = roomId
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
override var members: MXRoomMembers! {
|
override var members: MXRoomMembers! {
|
||||||
return mockRoomMembers
|
return mockRoomMembers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var roomId: String! {
|
||||||
|
return mockRoomId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeMXUpdatedRoomMembers: MXRoomMembers {
|
private class FakeMXUpdatedRoomMembers: MXRoomMembers {
|
||||||
@@ -202,12 +810,21 @@ private class FakeMXRoomMember: MXRoomMember {
|
|||||||
|
|
||||||
private class FakeMXEvent: MXEvent {
|
private class FakeMXEvent: MXEvent {
|
||||||
private var mockSender: String
|
private var mockSender: String
|
||||||
|
private var mockEventId: String?
|
||||||
|
|
||||||
init(sender: String) {
|
init(sender: String) {
|
||||||
mockSender = sender
|
mockSender = sender
|
||||||
|
mockEventId = nil
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(eventId: String, sender: String) {
|
||||||
|
mockEventId = eventId
|
||||||
|
mockSender = sender
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError()
|
fatalError()
|
||||||
@@ -217,4 +834,9 @@ private class FakeMXEvent: MXEvent {
|
|||||||
get { return mockSender }
|
get { return mockSender }
|
||||||
set { mockSender = newValue }
|
set { mockSender = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var eventId: String! {
|
||||||
|
get { return mockEventId }
|
||||||
|
set { mockEventId = newValue }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Permalinks to a room/space are pillified
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Permalinks to a matrix user are pillified
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Permalinks to messages are pillified
|
||||||
Reference in New Issue
Block a user