mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-30 21:26: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_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.";
|
||||
|
||||
// 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 locationShareIcon = ImageAsset(name: "location_share_icon")
|
||||
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 pollCheckboxSelected = ImageAsset(name: "poll_checkbox_selected")
|
||||
internal static let pollDeleteIcon = ImageAsset(name: "poll_delete_icon")
|
||||
|
||||
@@ -4691,6 +4691,22 @@ public class VectorL10n: NSObject {
|
||||
public static func photoLibraryAccessNotGranted(_ p1: String) -> String {
|
||||
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
|
||||
public static var pinProtectionChoosePin: String {
|
||||
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.
|
||||
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";
|
||||
|
||||
// Regex expression for permalink detection
|
||||
NSString *const kMXKToolsRegexStringForPermalink = @"\\/#\\/(?:(?:room|user)\\/)?([^\\s]*)";
|
||||
|
||||
|
||||
#pragma mark - MXKTools static private members
|
||||
// The regex used to find matrix ids.
|
||||
static NSRegularExpression *userIdRegex;
|
||||
@@ -47,6 +51,8 @@ static NSRegularExpression *httpLinksRegex;
|
||||
// A regex to find all HTML tags
|
||||
static NSRegularExpression *htmlTagsRegex;
|
||||
static NSDataDetector *linkDetector;
|
||||
// A regex to detect permalinks
|
||||
static NSRegularExpression* permalinkRegex;
|
||||
|
||||
@implementation MXKTools
|
||||
|
||||
@@ -63,6 +69,9 @@ static NSDataDetector *linkDetector;
|
||||
httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
|
||||
|
||||
NSString *permalinkPattern = [NSString stringWithFormat:@"%@%@", BuildSettings.clientPermalinkBaseUrl ?: kMXMatrixDotToUrl, kMXKToolsRegexStringForPermalink];
|
||||
permalinkRegex = [NSRegularExpression regularExpressionWithPattern:permalinkPattern options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1039,10 +1048,29 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
{
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex];
|
||||
}
|
||||
|
||||
// Permalinks
|
||||
NSArray* matches = [httpLinksRegex matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
if (matches) {
|
||||
for (NSTextCheckingResult *match in matches)
|
||||
{
|
||||
NSRange matchRange = [match range];
|
||||
|
||||
NSString *link = [mutableAttributedString.string substringWithRange:matchRange];
|
||||
// Handle potential permalinks
|
||||
if ([permalinkRegex numberOfMatchesInString:link options:0 range:NSMakeRange(0, link.length)]) {
|
||||
NSURLComponents *url = [[NSURLComponents new] initWithString:link];
|
||||
if (url.URL)
|
||||
{
|
||||
[mutableAttributedString addAttribute:NSLinkAttributeName value:url.URL range:matchRange];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This allows to check for normal url based links (like https://element.io)
|
||||
// And set back the default link color
|
||||
NSArray *matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
if (matches)
|
||||
{
|
||||
for (NSTextCheckingResult *match in matches)
|
||||
|
||||
@@ -25,7 +25,9 @@ class PillAttachmentView: UIView {
|
||||
struct Sizes {
|
||||
var verticalMargin: CGFloat
|
||||
var horizontalMargin: CGFloat
|
||||
var avatarLeading: CGFloat
|
||||
var avatarSideLength: CGFloat
|
||||
var itemSpacing: CGFloat
|
||||
|
||||
var pillBackgroundHeight: CGFloat {
|
||||
return avatarSideLength + 2 * verticalMargin
|
||||
@@ -33,11 +35,8 @@ class PillAttachmentView: UIView {
|
||||
var pillHeight: CGFloat {
|
||||
return pillBackgroundHeight + 2 * verticalMargin
|
||||
}
|
||||
var displaynameLabelLeading: CGFloat {
|
||||
return avatarSideLength + 2 * horizontalMargin
|
||||
}
|
||||
var totalWidthWithoutLabel: CGFloat {
|
||||
return displaynameLabelLeading + 2 * horizontalMargin
|
||||
return avatarSideLength + 2 * horizontalMargin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,44 +55,111 @@ class PillAttachmentView: UIView {
|
||||
mediaManager: MXMediaManager?,
|
||||
andPillData pillData: PillTextAttachmentData) {
|
||||
self.init(frame: frame)
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = pillData.displayText
|
||||
label.font = pillData.font
|
||||
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
|
||||
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: sizes.pillBackgroundHeight))
|
||||
label.frame = CGRect(x: sizes.displaynameLabelLeading,
|
||||
y: 0,
|
||||
width: labelSize.width,
|
||||
height: sizes.pillBackgroundHeight)
|
||||
|
||||
let stack = UIStackView(frame: frame)
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.spacing = sizes.itemSpacing
|
||||
|
||||
var computedWidth: CGFloat = 0
|
||||
for item in pillData.items {
|
||||
switch item {
|
||||
case .text(let string):
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = string
|
||||
label.font = pillData.font
|
||||
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
stack.addArrangedSubview(label)
|
||||
|
||||
computedWidth += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: sizes.pillBackgroundHeight)).width
|
||||
|
||||
case .avatar(let url, let alt, let matrixId):
|
||||
let avatarView = UserAvatarView(frame: CGRect(origin: .zero, size: CGSize(width: sizes.avatarSideLength, height: sizes.avatarSideLength)))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: matrixId,
|
||||
displayName: alt,
|
||||
avatarUrl: url,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(matrixId, alt)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.addArrangedSubview(avatarView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
avatarView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
|
||||
case .spaceAvatar(let url, let alt, let matrixId):
|
||||
let avatarView = SpaceAvatarView(frame: CGRect(origin: .zero, size: CGSize(width: sizes.avatarSideLength, height: sizes.avatarSideLength)))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: matrixId,
|
||||
displayName: alt,
|
||||
avatarUrl: url,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(matrixId, alt)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.addArrangedSubview(avatarView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
avatarView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
|
||||
case .asset(let name, let parameters):
|
||||
let assetView = UIView(frame: CGRect(x: 0, y: 0, width: sizes.avatarSideLength, height: sizes.avatarSideLength))
|
||||
assetView.backgroundColor = parameters.backgroundColor?.uiColor
|
||||
assetView.layer.cornerRadius = sizes.avatarSideLength / 2
|
||||
assetView.isUserInteractionEnabled = false
|
||||
assetView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let imageView = UIImageView(frame: .zero)
|
||||
imageView.image = ImageAsset(name: name).image.withRenderingMode(UIImage.RenderingMode(rawValue: parameters.rawRenderingMode) ?? .automatic)
|
||||
imageView.tintColor = parameters.tintColor?.uiColor ?? theme.baseIconPrimaryColor
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
|
||||
assetView.vc_addSubViewMatchingParent(imageView, withInsets: UIEdgeInsets(top: parameters.padding, left: parameters.padding, bottom: -parameters.padding, right: -parameters.padding))
|
||||
|
||||
stack.addArrangedSubview(assetView)
|
||||
NSLayoutConstraint.activate([
|
||||
assetView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
assetView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
}
|
||||
}
|
||||
computedWidth += max(0, CGFloat(stack.arrangedSubviews.count - 1) * stack.spacing)
|
||||
|
||||
let leadingStackMargin: CGFloat
|
||||
switch pillData.items.first {
|
||||
case .asset, .avatar:
|
||||
leadingStackMargin = sizes.avatarLeading
|
||||
computedWidth += sizes.avatarLeading + sizes.horizontalMargin
|
||||
default:
|
||||
leadingStackMargin = sizes.horizontalMargin
|
||||
computedWidth += 2 * sizes.horizontalMargin
|
||||
}
|
||||
|
||||
let pillBackgroundView = UIView(frame: CGRect(x: 0,
|
||||
y: sizes.verticalMargin,
|
||||
width: labelSize.width + sizes.totalWidthWithoutLabel,
|
||||
width: computedWidth,
|
||||
height: sizes.pillBackgroundHeight))
|
||||
|
||||
let avatarView = UserAvatarView(frame: CGRect(x: sizes.horizontalMargin,
|
||||
y: sizes.verticalMargin,
|
||||
width: sizes.avatarSideLength,
|
||||
height: sizes.avatarSideLength))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: pillData.matrixItemId,
|
||||
displayName: pillData.displayName,
|
||||
avatarUrl: pillData.avatarUrl,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(pillData.matrixItemId, pillData.displayName)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
|
||||
pillBackgroundView.addSubview(avatarView)
|
||||
pillBackgroundView.addSubview(label)
|
||||
|
||||
pillBackgroundView.vc_addSubViewMatchingParent(stack, withInsets: UIEdgeInsets(top: sizes.verticalMargin, left: leadingStackMargin, bottom: -sizes.verticalMargin, right: -sizes.horizontalMargin))
|
||||
|
||||
pillBackgroundView.backgroundColor = pillData.isHighlighted ? theme.colors.alert : theme.colors.quinaryContent
|
||||
pillBackgroundView.layer.cornerRadius = sizes.pillBackgroundHeight / 2.0
|
||||
|
||||
self.addSubview(pillBackgroundView)
|
||||
self.alpha = pillData.alpha
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Override
|
||||
override var isHidden: Bool {
|
||||
get {
|
||||
|
||||
@@ -20,9 +20,11 @@ import UIKit
|
||||
@available(iOS 15.0, *)
|
||||
@objc class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||
// MARK: - Properties
|
||||
private static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
|
||||
horizontalMargin: 4.0,
|
||||
avatarSideLength: 16.0)
|
||||
static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
|
||||
horizontalMargin: 6.0,
|
||||
avatarLeading: 2.0,
|
||||
avatarSideLength: 16.0,
|
||||
itemSpacing: 4)
|
||||
private weak var messageTextView: MXKMessageTextView?
|
||||
|
||||
// MARK: - Override
|
||||
@@ -47,8 +49,7 @@ import UIKit
|
||||
|
||||
let mainSession = AppDelegate.theDelegate().mxSessions.first as? MXSession
|
||||
|
||||
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: Self.size(forDisplayText: pillData.displayText,
|
||||
andFont: pillData.font)),
|
||||
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: textAttachment.size(forFont: pillData.font)),
|
||||
sizes: Self.pillAttachmentViewSizes,
|
||||
theme: ThemeService.shared().theme,
|
||||
mediaManager: mainSession?.mediaManager,
|
||||
@@ -57,23 +58,3 @@ import UIKit
|
||||
messageTextView?.registerPillView(pillView)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
extension PillAttachmentViewProvider {
|
||||
/// Computes size required to display a pill for given display text.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - displayText: display text for the pill
|
||||
/// - font: the text font
|
||||
/// - Returns: required size for pill
|
||||
static func size(forDisplayText displayText: String, andFont font: UIFont) -> CGSize {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = displayText
|
||||
label.font = font
|
||||
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: pillAttachmentViewSizes.pillBackgroundHeight))
|
||||
|
||||
return CGSize(width: labelSize.width + pillAttachmentViewSizes.totalWidthWithoutLabel,
|
||||
height: pillAttachmentViewSizes.pillHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
private enum PillAttachmentKind {
|
||||
case attachment(PillTextAttachment)
|
||||
case string(NSAttributedString)
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
struct PillProvider {
|
||||
private let session: MXSession
|
||||
private let eventFormatter: MXKEventFormatter
|
||||
private let event: MXEvent
|
||||
private let roomState: MXRoomState
|
||||
private let latestRoomState: MXRoomState?
|
||||
private let isEditMode: Bool
|
||||
|
||||
init(withSession session: MXSession,
|
||||
eventFormatter: MXKEventFormatter,
|
||||
event: MXEvent,
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?,
|
||||
isEditMode: Bool) {
|
||||
|
||||
self.session = session
|
||||
self.eventFormatter = eventFormatter
|
||||
self.event = event
|
||||
self.roomState = roomState
|
||||
self.latestRoomState = latestRoomState
|
||||
self.isEditMode = isEditMode
|
||||
}
|
||||
|
||||
func pillTextAttachmentString(forUrl url: URL, withLabel label: String, event: MXEvent) -> NSAttributedString? {
|
||||
|
||||
// Try to get a pill from this url
|
||||
guard let pillType = PillType.from(url: url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not pillify an url if it is a markdown or an http link (except for user and room) with a custom text
|
||||
|
||||
// First, we need to handle the case where the label can contains more than one # (room alias)
|
||||
var urlFromLabel = URL(string: label)?.absoluteURL
|
||||
if urlFromLabel == nil, label.filter({ $0 == "#" }).count > 1 {
|
||||
if let escapedLabel = label.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedLabel) {
|
||||
urlFromLabel = Tools.fixURL(withSeveralHashKeys: url)
|
||||
}
|
||||
}
|
||||
|
||||
let fixedUrl = Tools.fixURL(withSeveralHashKeys: url)
|
||||
let isUrlMarkDownLink = urlFromLabel != fixedUrl
|
||||
|
||||
let result: PillAttachmentKind
|
||||
switch pillType {
|
||||
case .user(let userId):
|
||||
var userFound = false
|
||||
result = pillTextAttachment(forUserId: userId, userFound: &userFound)
|
||||
// if it is a markdown link and we didn't found the user, don't pillify it
|
||||
if isUrlMarkDownLink && !userFound {
|
||||
return nil
|
||||
}
|
||||
case .room(let roomId):
|
||||
var roomFound = false
|
||||
result = pillTextAttachment(forRoomId: roomId, roomFound: &roomFound)
|
||||
// if it is a markdown link and we didn't found the room, don't pillify it
|
||||
if isUrlMarkDownLink && !roomFound {
|
||||
return nil
|
||||
}
|
||||
case .message(let roomId, let messageId):
|
||||
// if it is a markdown link, don't pillify it
|
||||
if isUrlMarkDownLink {
|
||||
return nil
|
||||
}
|
||||
result = pillTextAttachment(forMessageId: messageId, inRoomId: roomId)
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .attachment(let pillTextAttachment):
|
||||
return PillsFormatter.attributedStringWithAttachment(pillTextAttachment, link: isEditMode ? nil : url, font: eventFormatter.defaultTextFont)
|
||||
case .string(let attributedString):
|
||||
// if we don't have an attachment, use the fallback attributed string
|
||||
let newAttrString = NSMutableAttributedString(attributedString: attributedString)
|
||||
if let font = eventFormatter.defaultTextFont {
|
||||
newAttrString.addAttribute(.font, value: font, range: .init(location: 0, length: newAttrString.length))
|
||||
}
|
||||
newAttrString.addAttribute(.foregroundColor, value: ThemeService.shared().theme.colors.links, range: .init(location: 0, length: newAttrString.length))
|
||||
newAttrString.addAttribute(.link, value: url, range: .init(location: 0, length: newAttrString.length))
|
||||
return newAttrString
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the latest available `MXRoomMember` from given data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userId: the id of the user
|
||||
/// - Returns: the room member, if available
|
||||
private func roomMember(withUserId userId: String) -> MXRoomMember? {
|
||||
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
|
||||
}
|
||||
|
||||
/// Create a pill representation for a given user
|
||||
/// - Parameters:
|
||||
/// - userId: the user MatrixID
|
||||
/// - userFound: this flag will be set to true if a user is found locally with this userId
|
||||
/// - Returns: a pill attachment
|
||||
private func pillTextAttachment(forUserId userId: String, userFound: inout Bool) -> PillAttachmentKind {
|
||||
// Search for a room member matching this user id
|
||||
let roomMember = self.roomMember(withUserId: userId)
|
||||
var user: MXUser?
|
||||
|
||||
if roomMember == nil {
|
||||
// fallback on getting the user from the session's store
|
||||
user = session.user(withUserId: userId)
|
||||
}
|
||||
|
||||
|
||||
let avatarUrl = roomMember?.avatarUrl ?? user?.avatarUrl
|
||||
let displayName = roomMember?.displayname ?? user?.displayName ?? userId
|
||||
let isHighlighted = userId == session.myUserId
|
||||
|
||||
let avatar: PillTextAttachmentItem
|
||||
if roomMember == nil && user == nil {
|
||||
avatar = .asset(named: "pill_user",
|
||||
parameters: .init(tintColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.secondaryContent),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysOriginal.rawValue,
|
||||
padding: 0.0))
|
||||
} else {
|
||||
avatar = .avatar(url: avatarUrl,
|
||||
string: displayName,
|
||||
matrixId: userId)
|
||||
}
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .user(userId: userId),
|
||||
items: [
|
||||
avatar,
|
||||
.text(displayName)
|
||||
],
|
||||
isHighlighted: isHighlighted,
|
||||
alpha: 1.0,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
userFound = roomMember != nil || user != nil
|
||||
|
||||
if let attachment = PillTextAttachment(attachmentData: data) {
|
||||
return .attachment(attachment)
|
||||
}
|
||||
|
||||
return .string(NSMutableAttributedString(string: displayName))
|
||||
}
|
||||
|
||||
/// Create a pill representation for a given room
|
||||
/// - Parameters:
|
||||
/// - roomId: the room MXID or alias
|
||||
/// - roomFound: this flag will be set to true if a room is found locally with this roomId
|
||||
/// - Returns: a pill attachment
|
||||
private func pillTextAttachment(forRoomId roomId: String, roomFound: inout Bool) -> PillAttachmentKind {
|
||||
// Get the room matching this roomId
|
||||
let room = roomId.starts(with: "#") ? session.room(withAlias: roomId) : session.room(withRoomId: roomId)
|
||||
let displayName = room?.displayName ?? VectorL10n.pillRoomFallbackDisplayName
|
||||
|
||||
let avatar: PillTextAttachmentItem
|
||||
if let room {
|
||||
if session.spaceService.getSpace(withId: roomId) != nil {
|
||||
avatar = .spaceAvatar(url: room.avatarData.mxContentUri,
|
||||
string: displayName,
|
||||
matrixId: roomId)
|
||||
} else {
|
||||
avatar = .avatar(url: room.avatarData.mxContentUri,
|
||||
string: displayName,
|
||||
matrixId: roomId)
|
||||
}
|
||||
} else {
|
||||
avatar = .asset(named: "link_icon",
|
||||
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
|
||||
}
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .room(roomId: roomId),
|
||||
items: [
|
||||
avatar,
|
||||
.text(displayName)
|
||||
],
|
||||
isHighlighted: false,
|
||||
alpha: 1.0,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
roomFound = room != nil
|
||||
|
||||
if let attachment = PillTextAttachment(attachmentData: data) {
|
||||
return .attachment(attachment)
|
||||
}
|
||||
|
||||
return .string(NSMutableAttributedString(string: displayName))
|
||||
}
|
||||
|
||||
/// Create a pill representation for a message in a room
|
||||
/// - Parameters:
|
||||
/// - messageId: message eventId
|
||||
/// - roomId: roomId of the message
|
||||
/// - Returns: a pill attachment
|
||||
private func pillTextAttachment(forMessageId messageId: String, inRoomId roomId: String) -> PillAttachmentKind {
|
||||
|
||||
// Check if this is the current room
|
||||
if roomId == roomState.roomId {
|
||||
return pillTextAttachment(inCurrentRoomForMessageId: messageId)
|
||||
}
|
||||
|
||||
let room = session.room(withRoomId: roomId)
|
||||
|
||||
let avatar: PillTextAttachmentItem
|
||||
if let room {
|
||||
avatar = .avatar(url: room.avatarData.mxContentUri,
|
||||
string: room.displayName,
|
||||
matrixId: roomId)
|
||||
} else {
|
||||
avatar = .asset(named: "link_icon",
|
||||
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
|
||||
|
||||
}
|
||||
|
||||
let displayText = room?.displayName.flatMap { VectorL10n.pillMessageIn($0) } ?? VectorL10n.pillMessage
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .message(roomId: roomId, eventId: messageId),
|
||||
items: [
|
||||
avatar,
|
||||
.text(displayText)
|
||||
],
|
||||
isHighlighted: false,
|
||||
alpha: 1.0,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
if let attachment = PillTextAttachment(attachmentData: data) {
|
||||
return .attachment(attachment)
|
||||
}
|
||||
|
||||
return .string(NSMutableAttributedString(string: displayText))
|
||||
}
|
||||
|
||||
/// Create a pill representation for a message in the current room
|
||||
/// - Parameters:
|
||||
/// - messageId: message eventId
|
||||
/// - Returns: a pill attachment
|
||||
private func pillTextAttachment(inCurrentRoomForMessageId messageId: String) -> PillAttachmentKind {
|
||||
var roomMember: MXRoomMember?
|
||||
// If we have the event locally, try to get the room member
|
||||
if let event = session.store.event(withEventId: messageId, inRoom: roomState.roomId) {
|
||||
roomMember = self.roomMember(withUserId: event.sender)
|
||||
}
|
||||
|
||||
let displayText: String
|
||||
let avatar: PillTextAttachmentItem
|
||||
if let roomMember {
|
||||
displayText = VectorL10n.pillMessageFrom(roomMember.displayname)
|
||||
avatar = .avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId)
|
||||
} else {
|
||||
displayText = VectorL10n.pillMessage
|
||||
avatar = .asset(named: "link_icon",
|
||||
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
|
||||
}
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .message(roomId: roomState.roomId, eventId: messageId),
|
||||
items: [
|
||||
avatar,
|
||||
.text(displayText)
|
||||
].compactMap { $0 },
|
||||
isHighlighted: false,
|
||||
alpha: 1.0,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
if let attachment = PillTextAttachment(attachmentData: data) {
|
||||
return .attachment(attachment)
|
||||
}
|
||||
|
||||
return .string(NSMutableAttributedString(string: displayText))
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ class PillTextAttachment: NSTextAttachment {
|
||||
|
||||
updateBounds()
|
||||
}
|
||||
|
||||
convenience init?(attachmentData: PillTextAttachmentData) {
|
||||
guard let encodedData = try? Self.serializationService.serialize(attachmentData) else { return nil }
|
||||
self.init(data: encodedData, ofType: PillsFormatter.pillUTType)
|
||||
}
|
||||
|
||||
/// Create a Mention Pill text attachment for given room member.
|
||||
///
|
||||
@@ -55,9 +60,13 @@ class PillTextAttachment: NSTextAttachment {
|
||||
convenience init?(withRoomMember roomMember: MXRoomMember,
|
||||
isHighlighted: Bool,
|
||||
font: UIFont) {
|
||||
let data = PillTextAttachmentData(matrixItemId: roomMember.userId,
|
||||
displayName: roomMember.displayname,
|
||||
avatarUrl: roomMember.avatarUrl,
|
||||
let data = PillTextAttachmentData(pillType: .user(userId: roomMember.userId),
|
||||
items: [
|
||||
.avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId),
|
||||
.text(roomMember.displayname)
|
||||
],
|
||||
isHighlighted: isHighlighted,
|
||||
alpha: 1.0,
|
||||
font: font)
|
||||
@@ -71,14 +80,63 @@ class PillTextAttachment: NSTextAttachment {
|
||||
|
||||
updateBounds()
|
||||
}
|
||||
|
||||
/// Computes size required to display a pill for given display text.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - font: the text font
|
||||
/// - Returns: required size for pill
|
||||
func size(forFont font: UIFont) -> CGSize {
|
||||
guard let data else {
|
||||
MXLog.debug("[PillTextAttachment]: data are missing")
|
||||
return .zero
|
||||
}
|
||||
|
||||
let sizes = PillAttachmentViewProvider.pillAttachmentViewSizes
|
||||
|
||||
var width: CGFloat = 0
|
||||
|
||||
var textContent = ""
|
||||
for item in data.items {
|
||||
switch item {
|
||||
case .text(let text):
|
||||
textContent += text
|
||||
case .avatar, .asset, .spaceAvatar:
|
||||
width += sizes.avatarSideLength
|
||||
}
|
||||
}
|
||||
|
||||
// add texts
|
||||
if !textContent.isEmpty {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = font
|
||||
label.text = textContent
|
||||
width += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: sizes.pillBackgroundHeight)).width
|
||||
}
|
||||
|
||||
// add spacing
|
||||
width += CGFloat(max(0, data.items.count - 1)) * sizes.itemSpacing
|
||||
// add margins
|
||||
switch data.items.first {
|
||||
case .asset, .avatar:
|
||||
width += sizes.avatarLeading + sizes.horizontalMargin
|
||||
default:
|
||||
width += 2 * sizes.horizontalMargin
|
||||
}
|
||||
|
||||
return CGSize(width: width,
|
||||
height: sizes.pillHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@available (iOS 15.0, *)
|
||||
private extension PillTextAttachment {
|
||||
|
||||
func updateBounds() {
|
||||
guard let data = data else { return }
|
||||
let pillSize = PillAttachmentViewProvider.size(forDisplayText: data.displayText, andFont: data.font)
|
||||
let pillSize = size(forFont: data.font)
|
||||
// Offset to align pill centerY with text centerY.
|
||||
let offset = data.font.descender + (data.font.lineHeight - pillSize.height) / 2.0
|
||||
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: pillSize)
|
||||
|
||||
@@ -17,16 +17,55 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
struct PillAssetColor: Codable {
|
||||
var red: CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0
|
||||
|
||||
var uiColor: UIColor {
|
||||
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
|
||||
init(uiColor: UIColor) {
|
||||
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||
}
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
struct PillAssetParameter: Codable {
|
||||
var tintColor: PillAssetColor?
|
||||
var backgroundColor: PillAssetColor?
|
||||
var rawRenderingMode: Int = UIImage.RenderingMode.automatic.rawValue
|
||||
var padding: CGFloat = 2.0
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
enum PillTextAttachmentItem: Codable {
|
||||
case text(String)
|
||||
case avatar(url: String?, string: String?, matrixId: String)
|
||||
case spaceAvatar(url: String?, string: String?, matrixId: String)
|
||||
case asset(named: String, parameters: PillAssetParameter)
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
extension PillTextAttachmentItem {
|
||||
var string: String? {
|
||||
switch self {
|
||||
case .text(let text):
|
||||
return text
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data associated with a Pill text attachment.
|
||||
@available (iOS 15.0, *)
|
||||
struct PillTextAttachmentData: Codable {
|
||||
// MARK: - Properties
|
||||
/// Matrix item identifier (user id or room id)
|
||||
var matrixItemId: String
|
||||
/// Matrix item display name (user or room display name)
|
||||
var displayName: String?
|
||||
/// Matrix item avatar URL (user or room avatar url)
|
||||
var avatarUrl: String?
|
||||
/// Pill type
|
||||
var pillType: PillType
|
||||
/// Items to render
|
||||
var items: [PillTextAttachmentItem]
|
||||
/// Wether the pill should be highlighted
|
||||
var isHighlighted: Bool
|
||||
/// Alpha for pill display
|
||||
@@ -36,43 +75,36 @@ struct PillTextAttachmentData: Codable {
|
||||
|
||||
/// Helper for preferred text to display.
|
||||
var displayText: String {
|
||||
guard let displayName = displayName,
|
||||
displayName.count > 0 else {
|
||||
return matrixItemId
|
||||
}
|
||||
|
||||
return displayName
|
||||
return items.map { $0.string }
|
||||
.compactMap { $0 }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
/// Init.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - matrixItemId: Matrix item identifier (user id or room id)
|
||||
/// - displayName: Matrix item display name (user or room display name)
|
||||
/// - avatarUrl: Matrix item avatar URL (user or room avatar url)
|
||||
/// - pillType: Type for the pill
|
||||
/// - items: Items to display
|
||||
/// - isHighlighted: Wether the pill should be highlighted
|
||||
/// - alpha: Alpha for pill display
|
||||
/// - font: Font for the display name
|
||||
init(matrixItemId: String,
|
||||
displayName: String?,
|
||||
avatarUrl: String?,
|
||||
init(pillType: PillType,
|
||||
items: [PillTextAttachmentItem],
|
||||
isHighlighted: Bool,
|
||||
alpha: CGFloat,
|
||||
font: UIFont) {
|
||||
self.matrixItemId = matrixItemId
|
||||
self.displayName = displayName
|
||||
self.avatarUrl = avatarUrl
|
||||
self.pillType = pillType
|
||||
self.items = items
|
||||
self.isHighlighted = isHighlighted
|
||||
self.alpha = alpha
|
||||
self.font = font
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Codable
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case matrixItemId
|
||||
case displayName
|
||||
case avatarUrl
|
||||
case pillType
|
||||
case items
|
||||
case isHighlighted
|
||||
case alpha
|
||||
case font
|
||||
@@ -84,9 +116,8 @@ struct PillTextAttachmentData: Codable {
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
matrixItemId = try container.decode(String.self, forKey: .matrixItemId)
|
||||
displayName = try? container.decode(String.self, forKey: .displayName)
|
||||
avatarUrl = try? container.decode(String.self, forKey: .avatarUrl)
|
||||
pillType = try container.decode(PillType.self, forKey: .pillType)
|
||||
items = try container.decode([PillTextAttachmentItem].self, forKey: .items)
|
||||
isHighlighted = try container.decode(Bool.self, forKey: .isHighlighted)
|
||||
alpha = try container.decode(CGFloat.self, forKey: .alpha)
|
||||
let fontData = try container.decode(Data.self, forKey: .font)
|
||||
@@ -99,12 +130,36 @@ struct PillTextAttachmentData: Codable {
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(matrixItemId, forKey: .matrixItemId)
|
||||
try? container.encode(displayName, forKey: .displayName)
|
||||
try? container.encode(avatarUrl, forKey: .avatarUrl)
|
||||
try container.encode(pillType, forKey: .pillType)
|
||||
try container.encode(items, forKey: .items)
|
||||
try container.encode(isHighlighted, forKey: .isHighlighted)
|
||||
try container.encode(alpha, forKey: .alpha)
|
||||
let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false)
|
||||
try container.encode(fontData, forKey: .font)
|
||||
}
|
||||
|
||||
// MARK: - Pill representations
|
||||
var pillIdentifier: String {
|
||||
switch pillType {
|
||||
case .user(let userId):
|
||||
return userId
|
||||
case .room(let roomId):
|
||||
return roomId
|
||||
case .message(let roomId, let messageId):
|
||||
return "\(roomId)/\(messageId)"
|
||||
}
|
||||
}
|
||||
|
||||
var markdown: String {
|
||||
var permalink: String
|
||||
switch pillType {
|
||||
case .user(let userId):
|
||||
permalink = MXTools.permalinkToUser(withUserId: userId)
|
||||
case .room(let roomId):
|
||||
permalink = MXTools.permalink(toRoom: roomId)
|
||||
case .message(let roomId, let messageId):
|
||||
permalink = MXTools.permalink(toEvent: messageId, inRoom: roomId)
|
||||
}
|
||||
return "[\(displayText)](\(permalink))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
enum PillType: Codable {
|
||||
case user(userId: String) /// userId
|
||||
case room(roomId: String) /// roomId
|
||||
case message(roomId: String, eventId: String) // roomId, eventId
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
extension PillType {
|
||||
private static var regexPermalinkTarget: NSRegularExpression? = {
|
||||
let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl ?? kMXMatrixDotToUrl
|
||||
let pattern = #"\#(clientBaseUrl)/#/(?:(?:room|user)/)?((?:@|!|#)[^@!#/?\s]*)/?((?:\$)[^\$/?\s]*)?"#
|
||||
return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
|
||||
}()
|
||||
|
||||
static func from(url: URL) -> PillType? {
|
||||
guard let regex = regexPermalinkTarget else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var link = url.absoluteString
|
||||
// we need to remove percent encoding (it's possible that it has been encoded multiple times)
|
||||
while let cleaned = link.removingPercentEncoding, cleaned != link {
|
||||
link = cleaned
|
||||
}
|
||||
|
||||
let pills = regex.matches(in: link, options: [], range: NSRange(link.startIndex..., in: link))
|
||||
.map { result -> [String]? in
|
||||
guard result.numberOfRanges > 1 else { return nil }
|
||||
return (1..<result.numberOfRanges)
|
||||
.map { Range(result.range(at: $0), in: link) }
|
||||
.compactMap { $0 }
|
||||
.map { String(link[$0]).removingPercentEncoding }
|
||||
.compactMap { $0 }
|
||||
|
||||
}
|
||||
.compactMap { matrixIds -> PillType? in
|
||||
guard let matrixIds, !matrixIds.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
switch matrixIds[0].first {
|
||||
case "@":
|
||||
return .user(userId: matrixIds[0])
|
||||
case "!", "#":
|
||||
if matrixIds.count > 1 {
|
||||
if matrixIds[1].starts(with: "$") {
|
||||
return .message(roomId: matrixIds[0], eventId: matrixIds[1])
|
||||
}
|
||||
}
|
||||
return .room(roomId: matrixIds[0])
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return pills.first
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class PillsFormatter: NSObject {
|
||||
case identifier
|
||||
case markdown
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Internal Methods
|
||||
/// Insert text attachments for pills inside given message attributed string.
|
||||
///
|
||||
@@ -52,17 +52,21 @@ class PillsFormatter: NSObject {
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?,
|
||||
isEditMode: Bool = false) -> NSAttributedString {
|
||||
|
||||
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
||||
newAttr.vc_enumerateAttribute(.link) { (url: URL, range: NSRange, _) in
|
||||
if let userId = userIdFromPermalink(url.absoluteString),
|
||||
let roomMember = roomMember(withUserId: userId,
|
||||
roomState: roomState,
|
||||
andLatestRoomState: latestRoomState) {
|
||||
let isHighlighted = roomMember.userId == session.myUserId && event.sender != session.myUserId
|
||||
let attachmentString = mentionPill(withRoomMember: roomMember,
|
||||
andUrl: isEditMode ? nil : url,
|
||||
isHighlighted: isHighlighted,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
let provider = PillProvider(withSession: session,
|
||||
eventFormatter: eventFormatter,
|
||||
event: event,
|
||||
roomState: roomState,
|
||||
andLatestRoomState: latestRoomState,
|
||||
isEditMode: isEditMode)
|
||||
|
||||
// try to get a mention pill from the url
|
||||
let label = Range(range, in: newAttr.string).flatMap { String(newAttr.string[$0]) }
|
||||
if let attachmentString: NSAttributedString = provider.pillTextAttachmentString(forUrl: url, withLabel: label ?? "", event: event) {
|
||||
// replace the url with the pill
|
||||
newAttr.replaceCharacters(in: range, with: attachmentString)
|
||||
}
|
||||
}
|
||||
@@ -80,25 +84,27 @@ class PillsFormatter: NSObject {
|
||||
mode: PillsReplacementTextMode = .displayname) -> String {
|
||||
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
||||
newAttr.vc_enumerateAttribute(.attachment) { (attachment: PillTextAttachment, range: NSRange, _) in
|
||||
if let displayText = attachment.data?.displayText,
|
||||
let userId = attachment.data?.matrixItemId,
|
||||
let permalink = MXTools.permalinkToUser(withUserId: userId) {
|
||||
let pillString: String
|
||||
switch mode {
|
||||
case .displayname:
|
||||
pillString = displayText
|
||||
case .identifier:
|
||||
pillString = userId
|
||||
case .markdown:
|
||||
pillString = "[\(displayText)](\(permalink))"
|
||||
}
|
||||
newAttr.replaceCharacters(in: range, with: pillString)
|
||||
guard let data = attachment.data else {
|
||||
return
|
||||
}
|
||||
|
||||
let pillString: String
|
||||
switch mode {
|
||||
case .displayname:
|
||||
pillString = data.displayText
|
||||
case .identifier:
|
||||
pillString = data.pillIdentifier
|
||||
case .markdown:
|
||||
pillString = data.markdown
|
||||
}
|
||||
|
||||
newAttr.replaceCharacters(in: range, with: pillString)
|
||||
}
|
||||
|
||||
return newAttr.string
|
||||
}
|
||||
|
||||
|
||||
/// Creates an attributed string containing a pill for given room member.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -111,17 +117,13 @@ class PillsFormatter: NSObject {
|
||||
andUrl url: URL? = nil,
|
||||
isHighlighted: Bool,
|
||||
font: UIFont) -> NSAttributedString {
|
||||
|
||||
guard let attachment = PillTextAttachment(withRoomMember: roomMember, isHighlighted: isHighlighted, font: font) else {
|
||||
return NSAttributedString(string: roomMember.displayname)
|
||||
}
|
||||
let string = NSMutableAttributedString(attachment: attachment)
|
||||
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
|
||||
if let url = url {
|
||||
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
|
||||
}
|
||||
return string
|
||||
return attributedStringWithAttachment(attachment, link: url, font: font)
|
||||
}
|
||||
|
||||
|
||||
/// Update alpha of all `PillTextAttachment` contained in given attributed string.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -140,43 +142,37 @@ class PillsFormatter: NSObject {
|
||||
/// - roomState: room state for refresh, should be the latest available
|
||||
static func refreshPills(in attributedString: NSAttributedString, with roomState: MXRoomState) {
|
||||
attributedString.vc_enumerateAttribute(.attachment) { (pill: PillTextAttachment, range: NSRange, _) in
|
||||
guard let userId = pill.data?.matrixItemId,
|
||||
let roomMember = roomState.members.member(withUserId: userId) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch pill.data?.pillType {
|
||||
case .user(let userId):
|
||||
guard let roomMember = roomState.members.member(withUserId: userId) else {
|
||||
return
|
||||
}
|
||||
|
||||
pill.data?.displayName = roomMember.displayname
|
||||
pill.data?.avatarUrl = roomMember.avatarUrl
|
||||
pill.data?.items = [
|
||||
.avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId),
|
||||
.text(roomMember.displayname)
|
||||
]
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
@available (iOS 15.0, *)
|
||||
private extension PillsFormatter {
|
||||
/// Extract user id from given permalink
|
||||
/// - Parameter permalink: the permalink
|
||||
/// - Returns: userId, if any
|
||||
static func userIdFromPermalink(_ permalink: String) -> String? {
|
||||
let baseUrl: String
|
||||
if let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl {
|
||||
baseUrl = String(format: "%@/#/user/", clientBaseUrl)
|
||||
} else {
|
||||
baseUrl = String(format: "%@/#/", kMXMatrixDotToUrl)
|
||||
extension PillsFormatter {
|
||||
|
||||
static func attributedStringWithAttachment(_ attachment: PillTextAttachment, link: URL?, font: UIFont) -> NSAttributedString {
|
||||
let string = NSMutableAttributedString(attachment: attachment)
|
||||
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
|
||||
if let url = link {
|
||||
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
|
||||
}
|
||||
return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil
|
||||
}
|
||||
|
||||
/// Retrieve the latest available `MXRoomMember` from given data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userId: the id of the user
|
||||
/// - roomState: room state for message
|
||||
/// - latestRoomState: latest room state of the room containing this message
|
||||
/// - Returns: the room member, if available
|
||||
static func roomMember(withUserId userId: String,
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?) -> MXRoomMember? {
|
||||
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ final class SpaceAvatarView: AvatarView, NibOwnerLoadable {
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.avatarImageView.layer.cornerRadius = Constants.cornerRadius
|
||||
// Ensure we keep a rounded corner if the width is less than 2 * Constants.cornerRadius
|
||||
self.avatarImageView.layer.cornerRadius = max(2.0, min(self.avatarImageView.bounds.width / 4, Constants.cornerRadius))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@@ -23,6 +23,7 @@ final class UserAvatarView: AvatarView {
|
||||
|
||||
private func commonInit() {
|
||||
let avatarImageView = MXKImageView()
|
||||
avatarImageView.frame = self.frame
|
||||
self.vc_addSubViewMatchingParent(avatarImageView)
|
||||
self.avatarImageView = avatarImageView
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 aliceMember = FakeMXRoomMember(displayname: aliceDisplayname, avatarUrl: aliceAvatarUrl, userId: aliceUserId)
|
||||
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 mentionToAlice = NSAttributedString(string: aliceDisplayname, attributes: [.link: URL(string: 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
|
||||
@@ -47,11 +79,24 @@ class PillsFormatterTests: XCTestCase {
|
||||
// Attachment has correct type.
|
||||
XCTAssert(pillTextAttachment?.fileType == PillsFormatter.pillUTType)
|
||||
// Pill data contains Alice's displayname and avatar url.
|
||||
XCTAssertEqual(pillTextAttachment?.data?.displayText, Inputs.aliceDisplayname)
|
||||
XCTAssertEqual(pillTextAttachment?.data?.avatarUrl, Inputs.aliceAvatarUrl)
|
||||
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")
|
||||
}
|
||||
|
||||
// Pill has expected size.
|
||||
let expectedSize = PillAttachmentViewProvider.size(forDisplayText: pillTextAttachment!.data!.displayText,
|
||||
andFont: pillTextAttachment!.data!.font)
|
||||
let expectedSize = pillTextAttachment?.size(forFont: pillTextAttachment!.data!.font)
|
||||
XCTAssertEqual(pillTextAttachment?.bounds.size, expectedSize)
|
||||
|
||||
PillsFormatter.refreshPills(in: messageWithPills,
|
||||
@@ -60,11 +105,23 @@ class PillsFormatterTests: XCTestCase {
|
||||
// Alice's pill is still highlighted.
|
||||
XCTAssert(pillTextAttachment?.data?.isHighlighted == true)
|
||||
// Pill data is refreshed with correct data.
|
||||
XCTAssertEqual(refreshedPillTextAttachment?.data?.displayText, Inputs.aliceAwayDisplayname)
|
||||
XCTAssertEqual(refreshedPillTextAttachment?.data?.avatarUrl, Inputs.aliceNewAvatarUrl)
|
||||
let updatedPillTextAttachmentData: PillTextAttachmentData! = pillTextAttachment?.data
|
||||
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
|
||||
let newExpectedSize = PillAttachmentViewProvider.size(forDisplayText: refreshedPillTextAttachment!.data!.displayText,
|
||||
andFont: refreshedPillTextAttachment!.data!.font)
|
||||
let newExpectedSize = pillTextAttachment?.size(forFont: refreshedPillTextAttachment!.data!.font)
|
||||
XCTAssertEqual(refreshedPillTextAttachment?.bounds.size, newExpectedSize)
|
||||
}
|
||||
|
||||
@@ -72,8 +129,21 @@ class PillsFormatterTests: XCTestCase {
|
||||
let messageWithPills = createMessageWithMentionFromBobToAliceWithLatestRoomState()
|
||||
let pillTextAttachment = messageWithPills.attribute(.attachment, at: messageWithPills.length - 1, effectiveRange: nil) as? PillTextAttachment
|
||||
// Pill uses the latest room state data.
|
||||
XCTAssertEqual(pillTextAttachment?.data?.displayText, Inputs.aliceAwayDisplayname)
|
||||
XCTAssertEqual(pillTextAttachment?.data?.avatarUrl, Inputs.aliceNewAvatarUrl)
|
||||
XCTAssertNotNil(pillTextAttachment?.data)
|
||||
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() {
|
||||
@@ -89,6 +159,292 @@ class PillsFormatterTests: XCTestCase {
|
||||
XCTAssertEqual(messageWithDisplayname, Inputs.messageStart + Inputs.aliceDisplayname)
|
||||
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, *)
|
||||
@@ -105,6 +461,24 @@ private extension PillsFormatterTests {
|
||||
andLatestRoomState: nil)
|
||||
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 {
|
||||
let formattedMessage = NSMutableAttributedString(string: Inputs.messageStart)
|
||||
@@ -118,35 +492,269 @@ private extension PillsFormatterTests {
|
||||
andLatestRoomState: FakeMXRoomState(roomMembers: FakeMXUpdatedRoomMembers()))
|
||||
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
|
||||
private class FakeMXSession: MXSession {
|
||||
private var mockMyUserId: String
|
||||
|
||||
private var mockRooms: [FakeMXRoom] = []
|
||||
private var mockRoomSummaries: [String: FakeMXRoomSummary] = [:]
|
||||
private var mockStore: FakeMXStore?
|
||||
|
||||
init(myUserId: String) {
|
||||
mockMyUserId = myUserId
|
||||
|
||||
super.init()
|
||||
let credentials = MXCredentials(homeServer: "mock_home_server",
|
||||
userId: "mock_user_id",
|
||||
accessToken: "mock_access_token")
|
||||
let client = MXRestClient(credentials: credentials)
|
||||
super.init(matrixRestClient: client)
|
||||
}
|
||||
|
||||
override var myUserId: String! {
|
||||
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 let mockRoomMembers: MXRoomMembers
|
||||
private let mockRoomId: String?
|
||||
|
||||
init(roomMembers: MXRoomMembers) {
|
||||
mockRoomMembers = roomMembers
|
||||
mockRoomId = nil
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(roomMembers: MXRoomMembers, roomId: String) {
|
||||
mockRoomMembers = roomMembers
|
||||
mockRoomId = roomId
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override var members: MXRoomMembers! {
|
||||
return mockRoomMembers
|
||||
}
|
||||
|
||||
override var roomId: String! {
|
||||
return mockRoomId
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeMXUpdatedRoomMembers: MXRoomMembers {
|
||||
@@ -202,12 +810,21 @@ private class FakeMXRoomMember: MXRoomMember {
|
||||
|
||||
private class FakeMXEvent: MXEvent {
|
||||
private var mockSender: String
|
||||
private var mockEventId: String?
|
||||
|
||||
init(sender: String) {
|
||||
mockSender = sender
|
||||
mockEventId = nil
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(eventId: String, sender: String) {
|
||||
mockEventId = eventId
|
||||
mockSender = sender
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
@@ -217,4 +834,9 @@ private class FakeMXEvent: MXEvent {
|
||||
get { return mockSender }
|
||||
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