Skip to content

Commit

Permalink
Turning permalinks into pills
Browse files Browse the repository at this point in the history
#7409: Permalinks to a room/space are pillified
#7411: Permalinks to a matrix user are pillified
#7412: Permalinks to messages are pillified
  • Loading branch information
nimau committed Mar 17, 2023
1 parent 5a3adde commit 64ea190
Show file tree
Hide file tree
Showing 16 changed files with 1,420 additions and 166 deletions.
6 changes: 6 additions & 0 deletions Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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 %@";
16 changes: 16 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
31 changes: 30 additions & 1 deletion Riot/Modules/MatrixKit/Utils/MXKTools.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,6 +51,8 @@
// A regex to find all HTML tags
static NSRegularExpression *htmlTagsRegex;
static NSDataDetector *linkDetector;
// A regex to detect permalinks
static NSRegularExpression* permalinkRegex;

@implementation MXKTools

Expand All @@ -63,6 +69,9 @@ + (void)initialize
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];
});
}

Expand Down Expand Up @@ -1039,10 +1048,30 @@ + (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutable
{
[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)]) {
MXLogDebug(@"[MXKTools] Permalink detected: %@", link);
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)
Expand Down
143 changes: 112 additions & 31 deletions Riot/Modules/Pills/PillAttachmentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@ 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
}
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
}
}

Expand All @@ -56,44 +55,126 @@ 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
stack.translatesAutoresizingMaskIntoConstraints = false

let pillBackgroundView = UIView(frame: CGRect(x: 0,
y: sizes.verticalMargin,
width: labelSize.width + sizes.totalWidthWithoutLabel,
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

let avatarView = UserAvatarView(frame: CGRect(x: sizes.horizontalMargin,
y: sizes.verticalMargin,
width: sizes.avatarSideLength,
height: sizes.avatarSideLength))
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: pillData.matrixItemId,
displayName: pillData.displayName,
avatarUrl: pillData.avatarUrl,
mediaManager: mediaManager,
fallbackImage: .matrixItem(pillData.matrixItemId, pillData.displayName)))
avatarView.isUserInteractionEnabled = false
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)))

pillBackgroundView.addSubview(avatarView)
pillBackgroundView.addSubview(label)
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 assetView = UIView(frame: CGRect(x: 0, y: 0, width: sizes.avatarSideLength, height: sizes.avatarSideLength))
assetView.backgroundColor = theme.colors.links
assetView.layer.cornerRadius = sizes.avatarSideLength / 2
assetView.isUserInteractionEnabled = false
assetView.translatesAutoresizingMaskIntoConstraints = false

let imageView = UIImageView(frame: .zero)
imageView.image = ImageAsset(name: name).image.withRenderingMode(.alwaysTemplate)
imageView.tintColor = theme.baseIconPrimaryColor
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false

assetView.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: assetView.leadingAnchor, constant: 2),
imageView.trailingAnchor.constraint(equalTo: assetView.trailingAnchor, constant: -2),
imageView.topAnchor.constraint(equalTo: assetView.topAnchor, constant: 2),
imageView.bottomAnchor.constraint(equalTo: assetView.bottomAnchor, constant: -2)
])

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: computedWidth,
height: sizes.pillBackgroundHeight))

pillBackgroundView.addSubview(stack)

NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: pillBackgroundView.leadingAnchor, constant: leadingStackMargin),
stack.trailingAnchor.constraint(equalTo: pillBackgroundView.trailingAnchor, constant: -sizes.horizontalMargin),
stack.topAnchor.constraint(equalTo: pillBackgroundView.topAnchor, constant: sizes.verticalMargin),
stack.bottomAnchor.constraint(equalTo: pillBackgroundView.bottomAnchor, constant: -sizes.verticalMargin)
])

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 {
Expand Down
31 changes: 6 additions & 25 deletions Riot/Modules/Pills/PillAttachmentViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: 20.0,
itemSpacing: 4)
private weak var messageTextView: MXKMessageTextView?

// MARK: - Override
Expand All @@ -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,
Expand All @@ -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)
}
}
Loading

0 comments on commit 64ea190

Please sign in to comment.