Skip to content

Commit

Permalink
Merge pull request #7416 from vector-im/aringenbach/enable_rte_user_m…
Browse files Browse the repository at this point in the history
…entions

Enable user mentions in Rich Text Editor
  • Loading branch information
aringenbach authored Apr 14, 2023
2 parents 592b58a + fbe625f commit c1a9f31
Show file tree
Hide file tree
Showing 29 changed files with 639 additions and 135 deletions.
4 changes: 2 additions & 2 deletions Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
"state" : {
"revision" : "addf90f3e2a6ab46bd2b2febe117d9cddb646e7d",
"version" : "1.1.1"
"revision" : "758f226a92d6726ab626c1e78ecd183bdba77016",
"version" : "2.0.0"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,11 @@ typedef enum : NSUInteger
*/
@property (nonatomic) NSAttributedString *attributedTextMessage;

/**
Default font for the message composer.
*/
@property (nonatomic, readonly, nonnull) UIFont *defaultFont;

- (void)dismissValidationView:(MXKImageView*)validationView;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ - (void)pasteText:(NSString *)text
self.textMessage = [NSString stringWithFormat:@"%@%@", self.textMessage, text];
}

- (UIFont *)defaultFont
{
return [UIFont systemFontOfSize:15.f];
}

#pragma mark - MXKFileSizes

Expand Down
19 changes: 14 additions & 5 deletions Riot/Modules/Pills/PillAttachmentViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,29 @@ import UIKit
avatarLeading: 2.0,
avatarSideLength: 16.0,
itemSpacing: 4)
private weak var messageTextView: MXKMessageTextView?
private weak var messageTextView: UITextView?
private var pillViewFlusher: PillViewFlusher? {
messageTextView as? PillViewFlusher
}

// MARK: - Override
override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) {
super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location)

self.messageTextView = parentView?.superview as? MXKMessageTextView
// Keep a reference to the parent text view for size adjustments and pills flushing.
messageTextView = parentView?.superview as? UITextView
}

override func loadView() {
super.loadView()

guard let textAttachment = self.textAttachment as? PillTextAttachment else {
MXLog.debug("[PillAttachmentViewProvider]: attachment is missing or not of expected class")
MXLog.failure("[PillAttachmentViewProvider]: attachment is missing or not of expected class")
return
}

guard var pillData = textAttachment.data else {
MXLog.debug("[PillAttachmentViewProvider]: attachment misses pill data")
MXLog.failure("[PillAttachmentViewProvider]: attachment misses pill data")
return
}

Expand All @@ -59,6 +63,11 @@ import UIKit
mediaManager: mainSession?.mediaManager,
andPillData: pillData)
view = pillView
messageTextView?.registerPillView(pillView)

if let pillViewFlusher {
pillViewFlusher.registerPillView(pillView)
} else {
MXLog.failure("[PillAttachmentViewProvider]: no handler found, pill will not be flushed properly")
}
}
}
10 changes: 7 additions & 3 deletions Riot/Modules/Pills/PillProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ private enum PillAttachmentKind {
struct PillProvider {
private let session: MXSession
private let eventFormatter: MXKEventFormatter
private let event: MXEvent
private let event: MXEvent?
private let roomState: MXRoomState
private let latestRoomState: MXRoomState?
private let isEditMode: Bool

init(withSession session: MXSession,
eventFormatter: MXKEventFormatter,
event: MXEvent,
event: MXEvent?,
roomState: MXRoomState,
andLatestRoomState latestRoomState: MXRoomState?,
isEditMode: Bool) {
Expand All @@ -46,7 +46,7 @@ struct PillProvider {
self.isEditMode = isEditMode
}

func pillTextAttachmentString(forUrl url: URL, withLabel label: String, event: MXEvent) -> NSAttributedString? {
func pillTextAttachmentString(forUrl url: URL, withLabel label: String) -> NSAttributedString? {

// Try to get a pill from this url
guard let pillType = PillType.from(url: url) else {
Expand Down Expand Up @@ -133,6 +133,10 @@ struct PillProvider {
let avatarUrl = roomMember?.avatarUrl ?? user?.avatarUrl
let displayName = roomMember?.displayname ?? user?.displayName ?? userId
let isHighlighted = userId == session.myUserId
// No actual event means it is a composer Pill. No highlight
&& event != nil
// No highlight on self-mentions
&& event?.sender != session.myUserId

let avatar: PillTextAttachmentItem
if roomMember == nil && user == nil {
Expand Down
39 changes: 39 additions & 0 deletions Riot/Modules/Pills/PillViewFlusher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// 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 UIKit
import WysiwygComposer

/// Defines behaviour for an object that is able to manage views created
/// by a `NSTextAttachmentViewProvider`. This can be implemented
/// by an `UITextView` that would keep track of views in order to
/// (internally) clear them when required (e.g. when setting a new attributed text).
///
/// Note: It is necessary to clear views manually due to a bug in iOS. See `MXKMessageTextView`.
@available(iOS 15.0, *)
protocol PillViewFlusher: AnyObject {
/// Register a pill view that has been added through `NSTextAttachmentViewProvider`.
/// Should be called within the `loadView` function in order to clear the pills properly on text updates.
///
/// - Parameter pillView: View to register.
func registerPillView(_ pillView: UIView)
}

@available(iOS 15.0, *)
extension MXKMessageTextView: PillViewFlusher { }

@available(iOS 15.0, *)
extension WysiwygTextView: PillViewFlusher { }
86 changes: 84 additions & 2 deletions Riot/Modules/Pills/PillsFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class PillsFormatter: NSObject {

// 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) {
if let attachmentString: NSAttributedString = provider.pillTextAttachmentString(forUrl: url, withLabel: label ?? "") {
// replace the url with the pill
newAttr.replaceCharacters(in: range, with: attachmentString)
}
Expand All @@ -74,6 +74,41 @@ class PillsFormatter: NSObject {
return newAttr
}

/// Insert text attachments for pills inside given attributed string containing markdown.
///
/// - Parameters:
/// - markdownString: An attributed string with markdown formatting
/// - roomState: The current room state
/// - font: The font to use for the pill text
/// - Returns: A new attributed string with pills.
static func insertPills(in markdownString: NSAttributedString,
withSession session: MXSession,
eventFormatter: MXKEventFormatter,
roomState: MXRoomState,
font: UIFont) -> NSAttributedString {
let matches = markdownLinks(in: markdownString)

// If we have some matches, replace permalinks by a pill version.
guard !matches.isEmpty else { return markdownString }

let pillProvider = PillProvider(withSession: session,
eventFormatter: eventFormatter,
event: nil,
roomState: roomState,
andLatestRoomState: nil,
isEditMode: true)

let mutable = NSMutableAttributedString(attributedString: markdownString)

matches.reversed().forEach {
if let attachmentString = pillProvider.pillTextAttachmentString(forUrl: $0.url, withLabel: $0.label) {
mutable.replaceCharacters(in: $0.range, with: attachmentString)
}
}

return mutable
}

/// Creates a string with all pills of given attributed string replaced by display names.
///
/// - Parameters:
Expand Down Expand Up @@ -123,6 +158,20 @@ class PillsFormatter: NSObject {
}
return attributedStringWithAttachment(attachment, link: url, font: font)
}

static func mentionPill(withUrl url: URL,
andLabel label: String,
session: MXSession,
eventFormatter: MXKEventFormatter,
roomState: MXRoomState) -> NSAttributedString? {
let pillProvider = PillProvider(withSession: session,
eventFormatter: eventFormatter,
event: nil,
roomState: roomState,
andLatestRoomState: nil,
isEditMode: true)
return pillProvider.pillTextAttachmentString(forUrl: url, withLabel: label)
}

/// Update alpha of all `PillTextAttachment` contained in given attributed string.
///
Expand Down Expand Up @@ -160,12 +209,45 @@ class PillsFormatter: NSObject {
}
}
}

}

// MARK: - Private Methods
@available (iOS 15.0, *)
extension PillsFormatter {
struct MarkdownLinkResult: Equatable {
let url: URL
let label: String
let range: NSRange
}

static func markdownLinks(in attributedString: NSAttributedString) -> [MarkdownLinkResult] {
// Create a regexp that detects markdown links.
// Pattern source: https://gist.github.com/hugocf/66d6cd241eff921e0e02
let pattern = "\\[([^\\]]+)\\]\\(([^\\)\"\\s]+)(?:\\s+\"(.*)\")?\\)"
guard let regExp = try? NSRegularExpression(pattern: pattern) else { return [] }

let matches = regExp.matches(in: attributedString.string,
range: .init(location: 0, length: attributedString.length))

return matches.compactMap { match in
let labelRange = match.range(at: 1)
let urlRange = match.range(at: 2)
let label = attributedString.attributedSubstring(from: labelRange).string
var url = attributedString.attributedSubstring(from: urlRange).string

// Note: a valid markdown link can be written with
// enclosing <..>, remove them for userId detection.
if url.first == "<" && url.last == ">" {
url = String(url[url.index(after: url.startIndex)...url.index(url.endIndex, offsetBy: -2)])
}

if let url = URL(string: url) {
return MarkdownLinkResult(url: url, label: label, range: match.range)
} else {
return nil
}
}
}

static func attributedStringWithAttachment(_ attachment: PillTextAttachment, link: URL?, font: UIFont) -> NSAttributedString {
let string = NSMutableAttributedString(attachment: attachment)
Expand Down
15 changes: 15 additions & 0 deletions Riot/Modules/Room/RoomViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -5150,6 +5150,21 @@ - (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbar
[self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage];
}

- (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern
{
[self.userSuggestionCoordinator processSuggestionPattern:suggestionPattern];
}

- (UserSuggestionViewModelContextWrapper *)userSuggestionContext
{
return [self.userSuggestionCoordinator sharedContext];
}

- (MXMediaManager *)mediaManager
{
return self.roomDataSource.mxSession.mediaManager;
}

- (void)roomInputToolbarViewDidOpenActionMenu:(RoomInputToolbarView*)toolbarView
{
// Consider opening the action menu as beginning to type and share encryption keys if requested.
Expand Down
Loading

0 comments on commit c1a9f31

Please sign in to comment.