Merge pull request #6039 from vector-im/aringenbach/3526_user_pills

Add mention pills to timeline & composer
This commit is contained in:
aringenbach
2022-05-12 11:05:22 +02:00
committed by GitHub
35 changed files with 1576 additions and 345 deletions
@@ -1,33 +0,0 @@
/*
Copyright 2019 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;
NS_ASSUME_NONNULL_BEGIN
@interface UITextView(MatrixKit)
/**
Determine if there is a link near a location point in UITextView bounds.
@param point The point inside the UITextView bounds
@return YES to indicate that a link has been detected near the location point.
*/
- (BOOL)isThereALinkNearPoint:(CGPoint)point;
@end
NS_ASSUME_NONNULL_END
@@ -1,54 +0,0 @@
/*
Copyright 2019 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 "UITextView+MatrixKit.h"
@implementation UITextView(MatrixKit)
- (BOOL)isThereALinkNearPoint:(CGPoint)point
{
if (!CGRectContainsPoint(self.bounds, point))
{
return NO;
}
UITextPosition *textPosition = [self closestPositionToPoint:point];
if (!textPosition)
{
return NO;
}
UITextRange *textRange = [self.tokenizer rangeEnclosingPosition:textPosition
withGranularity:UITextGranularityCharacter
inDirection:UITextLayoutDirectionLeft];
if (!textRange)
{
return NO;
}
NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument toPosition:textRange.start];
if (startIndex < 0)
{
return NO;
}
return [self.attributedText attribute:NSLinkAttributeName atIndex:startIndex effectiveRange:NULL] != nil;
}
@end
@@ -0,0 +1,89 @@
//
// Copyright 2022 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
extension UITextView {
private enum Constants {
/// Distance threshold at which linkified text attachment can still be considered as "near" location.
static let attachmentLinkHorizontalDistanceThreshold: CGFloat = 16.0
}
/// Determine if there is a link near a location point in UITextView bounds.
///
/// - Parameters:
/// - point: The point inside the UITextView bounds
/// - Returns: true to indicate that a link has been detected near the location point.
@objc func isThereALinkNearLocation(_ point: CGPoint) -> Bool {
return urlForLinkAtLocation(point) != nil
}
/// Detect link near a location point in UITextView bounds.
///
/// - Parameter point: The point inside the UITextView bounds
/// - Returns: link detected at given location
@objc func urlForLinkAtLocation(_ point: CGPoint) -> URL? {
guard bounds.contains(point),
let textPosition = closestPosition(to: point)
else {
return nil
}
// The value of `NSLinkAttributeName` attribute could be an URL or a String object.
func attributeToLink(_ attribute: Any) -> URL? {
if let link = attribute as? URL {
return link
} else if let stringURL = attribute as? String {
return URL(string: stringURL)
} else {
return nil
}
}
// Depending on cursor position on a character containing both an attachment
// and a link (e.g. a mention pill), a positive result can be retrieved either
// from textStylingAtPosition or tokenizer's rangeEnclosingPosition.
if let attributes = textStyling(at: textPosition, in: .forward),
let linkAttribute = attributes[.link] {
// Using textStyling shouldn't provide false positives.
return attributeToLink(linkAttribute)
} else if let textRange = tokenizer.rangeEnclosingPosition(textPosition,
with: .character,
inDirection: .layout(.left)) {
let startIndex = offset(from: beginningOfDocument, to: textRange.start)
if let linkAttribute = attributedText.attribute(.link, at: startIndex, effectiveRange: nil) {
// Fix false positives from tokenizer's rangeEnclosingPosition.
// These occur if given point is located on the same line as a
// trailing linkified text attachment. Detected link is
// rejected if actual distance from attachment trailing to point
// is greater than linkHorizontalDistanceThreshold.
let glyphIndex = layoutManager.glyphIndexForCharacter(at: startIndex)
let attachmentWidth = layoutManager.attachmentSize(forGlyphAt: glyphIndex).width
// Width is -1 when there is no attachment.
if attachmentWidth > 0 {
let glyphStartX = layoutManager.location(forGlyphAt: glyphIndex).x
let start = glyphStartX - Constants.attachmentLinkHorizontalDistanceThreshold
let end = glyphStartX + attachmentWidth + Constants.attachmentLinkHorizontalDistanceThreshold
let range = (start...end)
return range.contains(point.x) ? attributeToLink(linkAttribute) : nil
}
}
}
return nil
}
}
@@ -170,6 +170,11 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey;
*/
@property (nonatomic) NSString *partialTextMessage;
/**
The current attributed text message partially typed in text input (use nil to reset it).
*/
@property (nonatomic) NSAttributedString *partialAttributedTextMessage;
/**
The current thread id for the data source. If provided, data source displays the specified thread, otherwise the whole room messages.
*/
@@ -471,6 +476,14 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey;
success:(void (^)(NSString *))success
failure:(void (^)(NSError *))failure;
/**
Updates an event with replacement event.
@note the original event is defined in the `MXEventContentRelatesTo` object.
@param replaceEvent the new event to display
*/
- (void)updateEventWithReplaceEvent:(MXEvent*)replaceEvent;
/**
Indicates if replying to the provided event is supported.
Only event of type 'MXEventTypeRoomMessage' are supported for the moment, and for certain msgtype.
@@ -735,6 +748,25 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey;
roomState:(MXRoomState*)roomState
direction:(MXTimelineDirection)direction;
/**
Queue an event in order to process its display later.
@param event the event to process.
@param roomState the state of the room when the event fired.
@param direction the order of the events in the arrays
*/
- (void)queueEventForProcessing:(MXEvent*)event
withRoomState:(MXRoomState*)roomState
direction:(MXTimelineDirection)direction;
/**
Start processing pending events.
@param onComplete a block called (on the main thread) when the processing has been done. Can be nil.
Note this block returns the number of added cells in first and last positions.
*/
- (void)processQueuedEvents:(void (^)(NSUInteger addedHistoryCellNb, NSUInteger addedLiveCellNb))onComplete;
#pragma mark - Bubble collapsing
/**
@@ -31,7 +31,6 @@
#import "MXKAppSettings.h"
#import "MXKSendReplyEventStringLocalizer.h"
#import "MXKSlashCommands.h"
#import "GeneratedInterface-Swift.h"
@@ -991,6 +990,16 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
_room.partialTextMessage = partialTextMessage;
}
- (NSAttributedString *)partialAttributedTextMessage
{
return _room.partialAttributedTextMessage;
}
- (void)setPartialAttributedTextMessage:(NSAttributedString *)partialAttributedTextMessage
{
_room.partialAttributedTextMessage = partialAttributedTextMessage;
}
- (void)refreshEventListeners:(NSArray *)liveEventTypesFilterForMessages
{
// Remove the existing listeners
@@ -2982,13 +2991,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
return processingQueue;
}
/**
Queue an event in order to process its display later.
@param event the event to process.
@param roomState the state of the room when the event fired.
@param direction the order of the events in the arrays
*/
- (void)queueEventForProcessing:(MXEvent*)event withRoomState:(MXRoomState*)roomState direction:(MXTimelineDirection)direction
{
if (event.isLocalEvent)
@@ -3166,12 +3168,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
return isHighlighted;
}
/**
Start processing pending events.
@param onComplete a block called (on the main thread) when the processing has been done. Can be nil.
Note this block returns the number of added cells in first and last positions.
*/
- (void)processQueuedEvents:(void (^)(NSUInteger addedHistoryCellNb, NSUInteger addedLiveCellNb))onComplete
{
MXWeakify(self);
@@ -1,25 +0,0 @@
/*
Copyright 2018 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/Foundation.h>
#import <MatrixSDK/MXSendReplyEventStringLocalizerProtocol.h>
/**
A `MXKSendReplyEventStringLocalizer` instance represents string localizations used when send reply event to a message in a room.
*/
@interface MXKSendReplyEventStringLocalizer : NSObject<MXSendReplyEventStringLocalizerProtocol>
@end
@@ -1,58 +0,0 @@
/*
Copyright 2018 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 "MXKSendReplyEventStringLocalizer.h"
#import "MXKSwiftHeader.h"
@implementation MXKSendReplyEventStringLocalizer
- (NSString *)senderSentAnImage
{
return [VectorL10n messageReplyToSenderSentAnImage];
}
- (NSString *)senderSentAVideo
{
return [VectorL10n messageReplyToSenderSentAVideo];
}
- (NSString *)senderSentAnAudioFile
{
return [VectorL10n messageReplyToSenderSentAnAudioFile];
}
- (NSString *)senderSentAVoiceMessage
{
return [VectorL10n messageReplyToSenderSentAVoiceMessage];
}
- (NSString *)senderSentAFile
{
return [VectorL10n messageReplyToSenderSentAFile];
}
- (NSString *)senderSentTheirLocation
{
return [VectorL10n messageReplyToSenderSentTheirLocation];
}
- (NSString *)messageToReplyToPrefix
{
return [VectorL10n messageReplyToMessageToReplyToPrefix];
}
@end
@@ -0,0 +1,47 @@
//
// Copyright 2022 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
class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalizerProtocol {
func senderSentAnImage() -> String {
return VectorL10n.messageReplyToSenderSentAnImage
}
func senderSentAVideo() -> String {
return VectorL10n.messageReplyToSenderSentAVideo
}
func senderSentAnAudioFile() -> String {
return VectorL10n.messageReplyToSenderSentAnAudioFile
}
func senderSentAVoiceMessage() -> String {
return VectorL10n.messageReplyToSenderSentAVoiceMessage
}
func senderSentAFile() -> String {
return VectorL10n.messageReplyToSenderSentAFile
}
func senderSentTheirLocation() -> String {
return VectorL10n.messageReplyToSenderSentTheirLocation
}
func messageToReplyToPrefix() -> String {
return VectorL10n.messageReplyToMessageToReplyToPrefix
}
}
@@ -228,6 +228,18 @@ typedef enum : NSUInteger {
*/
- (NSAttributedString*)renderHTMLString:(NSString*)htmlString forEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState;
/**
Render a random html string into an attributed string with the font and the text color
that correspond to the passed event.
@param htmlString the HTLM string to render.
@param event the event associated to the string.
@param roomState the room state right before the event.
@param isEditMode wether string will be used for edition in the composer
@return an attributed string.
*/
- (NSAttributedString*)renderHTMLString:(NSString*)htmlString forEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState isEditMode:(BOOL)isEditMode;
/**
Same as [self renderString:forEvent:] but add a prefix.
The prefix will be rendered with 'prefixTextFont' and 'prefixTextColor'.
@@ -1750,11 +1750,17 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
}
- (NSAttributedString*)renderHTMLString:(NSString*)htmlString forEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState
{
return [self renderHTMLString:htmlString forEvent:event withRoomState:roomState isEditMode:NO];
}
- (NSAttributedString*)renderHTMLString:(NSString*)htmlString forEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState isEditMode:(BOOL)isEditMode
{
NSString *html = htmlString;
// Special treatment for "In reply to" message
if (event.isReplyEvent || (!RiotSettings.shared.enableThreads && event.isInThread))
// Note: `isEditMode` fixes an issue where editing a reply would display an "In reply to" span instead of a mention.
if (!isEditMode && (event.isReplyEvent || (!RiotSettings.shared.enableThreads && event.isInThread)))
{
html = [self renderReplyTo:html withRoomState:roomState];
}
@@ -15,7 +15,7 @@
*/
#import "MXKMessageTextView.h"
#import "UITextView+MatrixKit.h"
#import "GeneratedInterface-Swift.h"
@interface MXKMessageTextView()
@@ -51,7 +51,7 @@
return NO;
}
return [self isThereALinkNearPoint:point];
return [self isThereALinkNearLocation:point];
}
@end
@@ -269,6 +269,11 @@ typedef enum : NSUInteger
*/
- (IBAction)onTouchUpInside:(UIButton*)button;
/**
Send message currently displayed inside toolbar's ` UITextView`.
*/
- (void)sendCurrentMessage;
/**
Handle image attachment
Save the image in user's photos library when 'isPhotoLibraryAsset' flag is NO and auto saving is enabled.
@@ -277,22 +277,27 @@
}
else if (button == self.rightInputToolbarButton && self.textMessage.length)
{
// This forces an autocorrect event to happen when "Send" is pressed, which is necessary to accept a pending correction on send
self.textMessage = [NSString stringWithFormat:@"%@ ", self.textMessage];
self.textMessage = [self.textMessage substringToIndex:[self.textMessage length]-1];
[self sendCurrentMessage];
}
}
NSString *message = self.textMessage;
// Reset message, disable view animation during the update to prevent placeholder distorsion.
[UIView setAnimationsEnabled:NO];
self.textMessage = nil;
[UIView setAnimationsEnabled:YES];
// Send button has been pressed
if (message.length && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendTextMessage:)])
{
[self.delegate roomInputToolbarView:self sendTextMessage:message];
}
- (void)sendCurrentMessage
{
// This forces an autocorrect event to happen when "Send" is pressed, which is necessary to accept a pending correction on send
self.textMessage = [NSString stringWithFormat:@"%@ ", self.textMessage];
self.textMessage = [self.textMessage substringToIndex:[self.textMessage length]-1];
NSString *message = self.textMessage;
// Reset message, disable view animation during the update to prevent placeholder distorsion.
[UIView setAnimationsEnabled:NO];
self.textMessage = nil;
[UIView setAnimationsEnabled:YES];
// Send button has been pressed
if (message.length && [self.delegate respondsToSelector:@selector(roomInputToolbarView:sendTextMessage:)])
{
[self.delegate roomInputToolbarView:self sendTextMessage:message];
}
}