2020-02-22 15:38:54 +04:00

919 lines
38 KiB
Objective-C

#import "HPGrowingTextView.h"
#import "LegacyComponentsInternal.h"
#import "TGFont.h"
#import "HPTextViewInternal.h"
#import "TGInputTextTag.h"
#import "TGMessage.h"
#import "TGColor.h"
NSString *TGMentionUidAttributeName = @"TGMentionUidAttributeName";
NSString *TGMentionBoldAttributeName = @"TGMentionBoldAttributeName";
@implementation TGAttributedTextRange
- (instancetype)initWithAttachment:(id)attachment {
self = [super init];
if (self != nil) {
_attachment = attachment;
}
return self;
}
@end
@interface HPGrowingTextView ()
{
UIColor *_intrinsicTextColor;
UIFont *_intrinsicTextFont;
__weak TGKeyCommandController *_keyCommandController;
}
@end
@implementation HPGrowingTextView
- (instancetype)initWithKeyCommandController:(TGKeyCommandController *)keyCommandController
{
self = [super initWithFrame:CGRectZero];
if (self != nil)
{
_keyCommandController = keyCommandController;
[self commonInitialiser];
}
return self;
}
- (NSDictionary *)defaultAttributes {
if (_intrinsicTextFont == nil) {
return @{NSFontAttributeName: TGSystemFontOfSize(17)};
} else {
if (_intrinsicTextColor)
return @{NSFontAttributeName: _intrinsicTextFont, NSForegroundColorAttributeName: _intrinsicTextColor};
else
return @{NSFontAttributeName: _intrinsicTextFont};
}
}
- (void)commonInitialiser
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
[HPTextViewInternal addTextViewMethods];
});
CGRect frame = self.frame;
frame.origin = CGPointZero;
_internalTextView = [[HPTextViewInternal alloc] initWithKeyCommandController:_keyCommandController];
_internalTextView.frame = frame;
_internalTextView.delegate = self;
_internalTextView.contentInset = UIEdgeInsetsZero;
_internalTextView.showsHorizontalScrollIndicator = NO;
_internalTextView.attributedText = [[NSAttributedString alloc] initWithString:@"-" attributes:[self defaultAttributes]];
_internalTextView.scrollsToTop = false;
if (iosMajorVersion() >= 7) {
_internalTextView.textContainer.layoutManager.allowsNonContiguousLayout = true;
_internalTextView.allowsEditingTextAttributes = true;
}
[self addSubview:_internalTextView];
_minHeight = _internalTextView.frame.size.height;
_minNumberOfLines = 1;
_animateHeightChange = true;
_animationDuration = 0.1f;
_internalTextView.attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:[self defaultAttributes]];
}
- (void)setDisableFormatting:(bool)disableFormatting
{
_disableFormatting = disableFormatting;
if (iosMajorVersion() >= 7)
_internalTextView.allowsEditingTextAttributes = !disableFormatting;
}
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
frame.origin = CGPointZero;
_internalTextView.frame = frame;
}
- (CGSize)sizeThatFits:(CGSize)size
{
if (self.attributedText.length == 0)
size.height = _minHeight;
return size;
}
- (void)setMaxNumberOfLines:(int)maxNumberOfLines
{
if (maxNumberOfLines == 0 && _maxHeight > 0) // the user specified a maxHeight themselves.
return;
// Use internalTextView for height calculations, thanks to Gwynne <http://blog.darkrainfall.org/>
NSAttributedString *saveText = _internalTextView.attributedText;
NSMutableAttributedString *newText = [[NSMutableAttributedString alloc] initWithString:@"-" attributes:[self defaultAttributes]];
_internalTextView.delegate = nil;
_internalTextView.hidden = YES;
for (int i = 1; i < maxNumberOfLines; ++i) {
[newText appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n|W|"]];
}
_internalTextView.attributedText = newText;
_maxHeight = [self measureHeight];
_internalTextView.attributedText = saveText;
_internalTextView.hidden = NO;
_internalTextView.delegate = self;
[self sizeToFit];
_maxNumberOfLines = maxNumberOfLines;
}
- (void)setMaxHeight:(CGFloat)maxHeight
{
_maxHeight = maxHeight;
_maxNumberOfLines = 0;
}
- (void)setMinNumberOfLines:(int)minNumberOfLines
{
if (minNumberOfLines == 0 && _minHeight > 0) // the user specified a minHeight themselves.
return;
// Use internalTextView for height calculations, thanks to Gwynne <http://blog.darkrainfall.org/>
NSAttributedString *saveText = _internalTextView.attributedText;
NSMutableAttributedString *newText = [[NSMutableAttributedString alloc] initWithString:@"-" attributes:[self defaultAttributes]];
_internalTextView.delegate = nil;
_internalTextView.hidden = YES;
for (int i = 1; i < minNumberOfLines; ++i) {
[newText appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n|W|"]];
}
_internalTextView.attributedText = newText;
_minHeight = [self measureHeight];
_internalTextView.attributedText = saveText;
_internalTextView.hidden = NO;
_internalTextView.delegate = self;
[self sizeToFit];
_minNumberOfLines = minNumberOfLines;
}
- (void)setMinHeight:(CGFloat)minHeight
{
_minHeight = minHeight;
_minNumberOfLines = 0;
}
- (void)textViewDidChange:(UITextView *)__unused textView
{
[self refreshAttributes];
[self refreshHeight:true];
if (self.showPlaceholderWhenFocussed)
_placeholderView.hidden = [_internalTextView hasText];
}
- (void)textViewDidChangeSelection:(UITextView *)__unused textView
{
id<HPGrowingTextViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(growingTextViewDidChangeSelection:)])
[delegate growingTextViewDidChangeSelection:self];
//_internalTextView.typingAttributes = [self defaultAttributes];
if ([_internalTextView selectedRange].length == 0) {
//[self refreshAttributes];
}
}
- (void)refreshHeight:(bool)textChanged
{
CGFloat newSizeH = [self measureHeight]; //size of content, so we can set the frame of self
if(newSizeH < _minHeight || !_internalTextView.hasText)
newSizeH = _minHeight; //not smalles than minHeight
if (_internalTextView.frame.size.height > _maxHeight)
newSizeH = _maxHeight; // not taller than maxHeight
id<HPGrowingTextViewDelegate> delegate = _delegate;
if (ABS(_internalTextView.frame.size.height - newSizeH) > FLT_EPSILON || _oneTimeLongAnimation)
{
// [fixed] Pasting too much text into the view failed to fire the height change,
// thanks to Gwynne <http://blog.darkrainfall.org/>
if (newSizeH > _maxHeight && _internalTextView.frame.size.height <= _maxHeight)
newSizeH = _maxHeight;
if (newSizeH <= _maxHeight)
{
if (_animateHeightChange && !_internalTextView.isPasting)
{
NSTimeInterval currentAnimationDuration = 0.12;
if (_oneTimeLongAnimation)
{
_oneTimeLongAnimation = false;
currentAnimationDuration = 0.3;
if (iosMajorVersion() < 7)
currentAnimationDuration *= 0.7;
}
[UIView animateWithDuration:currentAnimationDuration delay:0 options:(UIViewAnimationOptionAllowUserInteraction| UIViewAnimationOptionBeginFromCurrentState) animations:^
{
[self resizeTextView:newSizeH];
} completion:nil];
if ([delegate respondsToSelector:@selector(growingTextView:willChangeHeight:duration:animationCurve:)])
[delegate growingTextView:self willChangeHeight:newSizeH duration:currentAnimationDuration animationCurve:0];
}
else
{
[self resizeTextView:newSizeH];
if ([delegate respondsToSelector:@selector(growingTextView:willChangeHeight:duration:animationCurve:)])
[delegate growingTextView:self willChangeHeight:newSizeH duration:0.0 animationCurve:0];
}
}
// scroll to caret (needed on iOS7)
if (iosMajorVersion() >= 7)
{
/*NSRange range = _internalTextView.selectedRange;
[_internalTextView _scrollRangeToVisible:range animated:false];
CGRect r = [_internalTextView caretRectForPosition:_internalTextView.selectedTextRange.end];
CGFloat frameHeight = _internalTextView.frame.size.height;
CGFloat caretY = MAX(r.origin.y - frameHeight + r.size.height + 8, 0);
if (r.origin.y != INFINITY)
{
CGPoint contentOffset = _internalTextView.contentOffset;
contentOffset.y = caretY;
_internalTextView.contentOffset = contentOffset;
}*/
}
}
if (textChanged && [delegate respondsToSelector:@selector(growingTextViewDidChange:afterSetText:afterPastingText:)]) {
[delegate growingTextViewDidChange:self afterSetText:_ignoreChangeNotification afterPastingText:_internalTextView.isPasting];
}
_oneTimeLongAnimation = false;
}
- (void)notifyHeight {
id<HPGrowingTextViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(growingTextView:willChangeHeight:duration:animationCurve:)])
[delegate growingTextView:self willChangeHeight:_internalTextView.frame.size.height duration:0.0 animationCurve:0];
}
// Code from apple developer forum - @Steve Krulewitz, @Mark Marszal, @Eric Silverberg
- (CGFloat)measureHeight
{
if (iosMajorVersion() >= 7)
{
CGRect frame = _internalTextView.bounds;
CGSize fudgeFactor = CGSizeMake(10.0, 17.0);
frame.size.height -= fudgeFactor.height;
frame.size.width -= fudgeFactor.width;
frame.size.width -= _internalTextView.textContainerInset.right;
NSMutableAttributedString *textToMeasure = [[NSMutableAttributedString alloc] initWithAttributedString:_internalTextView.attributedText];
if ([textToMeasure.string hasSuffix:@"\n"])
{
[textToMeasure appendAttributedString:[[NSAttributedString alloc] initWithString:@"-"]];
}
[textToMeasure removeAttribute:NSFontAttributeName range:NSMakeRange(0, textToMeasure.length)];
if (_intrinsicTextFont != nil) {
[textToMeasure addAttribute:NSFontAttributeName value:_intrinsicTextFont range:NSMakeRange(0, textToMeasure.length)];
}
// NSString class method: boundingRectWithSize:options:attributes:context is
// available only on ios7.0 sdk.
CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin
//attributes:attributes
context:nil];
return CGFloor(CGRectGetHeight(size) + fudgeFactor.height);
}
else
{
return CGFloor(self.internalTextView.contentSize.height);
}
}
- (void)resizeTextView:(CGFloat)newSizeH
{
CGRect internalTextViewFrame = self.frame;
internalTextViewFrame.size.height = CGFloor(newSizeH);
self.frame = internalTextViewFrame;
internalTextViewFrame.origin = CGPointZero;
if(!CGRectEqualToRect(_internalTextView.frame, internalTextViewFrame))
_internalTextView.frame = internalTextViewFrame;
//[_internalTextView textViewEnsureSelectionVisible];
}
- (BOOL)becomeFirstResponder
{
return [_internalTextView becomeFirstResponder];
}
- (BOOL)resignFirstResponder
{
return [_internalTextView resignFirstResponder];
}
- (BOOL)isFirstResponder
{
return [_internalTextView isFirstResponder];
}
- (BOOL)canBecomeFirstResponder
{
return [_internalTextView canBecomeFirstResponder];
}
- (void)setText:(NSString *)newText
{
[self setText:newText animated:true];
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
[self setAttributedText:attributedText animated:true];
}
- (NSAttributedString *)attributedText {
return _internalTextView.attributedText;
}
- (void)setText:(NSString *)newText animated:(bool)animated {
[self setAttributedText:[[NSAttributedString alloc] initWithString:newText == nil ? @"" : newText attributes:[self defaultAttributes]] animated:animated];
}
- (void)setAttributedText:(NSAttributedString *)newText animated:(bool)animated {
[self setAttributedText:newText keepFormatting:false animated:animated];
}
- (void)setAttributedText:(NSAttributedString *)newText keepFormatting:(bool)keepFormatting animated:(bool)animated {
NSMutableAttributedString *fixedFontString = [[NSMutableAttributedString alloc] initWithAttributedString:newText];
if (!keepFormatting)
{
[fixedFontString removeAttribute:NSFontAttributeName range:NSMakeRange(0, fixedFontString.length)];
[fixedFontString addAttribute:NSFontAttributeName value:_intrinsicTextFont == nil ? [UIFont systemFontOfSize:17.0f] : _intrinsicTextFont range:NSMakeRange(0, fixedFontString.length)];
}
_internalTextView.attributedText = fixedFontString;
_internalTextView.typingAttributes = [self defaultAttributes];
[self refreshAttributes];
_placeholderView.hidden = fixedFontString.length != 0 || [_internalTextView isFirstResponder];
// include this line to analyze the height of the textview.
// fix from Ankit Thakur
bool previousAnimateHeightChange = _animateHeightChange;
_animateHeightChange = animated;
_ignoreChangeNotification = true;
[self performSelector:@selector(textViewDidChange:) withObject:_internalTextView];
_ignoreChangeNotification = false;
_animateHeightChange = previousAnimateHeightChange;
}
- (void)selectRange:(NSRange)range force:(bool)force {
if (range.length != 0 || force) {
UITextPosition *startPosition = [_internalTextView positionFromPosition:_internalTextView.beginningOfDocument offset:range.location];
UITextPosition *endPosition = [_internalTextView positionFromPosition:_internalTextView.beginningOfDocument offset:range.location + range.length];
UITextRange *selection = [_internalTextView textRangeFromPosition:startPosition toPosition:endPosition];
_internalTextView.selectedTextRange = selection;
}
}
-(NSString *)text
{
return _internalTextView.text;
}
- (void)setFont:(UIFont *)afont
{
_internalTextView.font = afont;
_intrinsicTextFont = afont;
[self setMaxNumberOfLines:_maxNumberOfLines];
[self setMinNumberOfLines:_minNumberOfLines];
}
- (UIFont *)font
{
return _intrinsicTextFont;
}
- (void)setTextColor:(UIColor *)color
{
_internalTextView.textColor = color;
_intrinsicTextColor = color;
}
- (UIColor *)textColor
{
return _internalTextView.textColor;
}
- (UIColor *)accentColor
{
if (_accentColor != nil)
return _accentColor;
return TGAccentColor();
}
- (void)setTextAlignment:(NSTextAlignment)aligment
{
_internalTextView.textAlignment = aligment;
}
- (NSTextAlignment)textAlignment
{
return _internalTextView.textAlignment;
}
#pragma mark -
- (BOOL)textViewShouldBeginEditing:(UITextView *)__unused textView
{
id<HPGrowingTextViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(growingTextViewShouldBeginEditing:)])
return [delegate growingTextViewShouldBeginEditing:self];
return true;
}
- (void)textViewDidBeginEditing:(UITextView *)__unused textView
{
id<HPGrowingTextViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(growingTextViewDidBeginEditing:)])
[delegate growingTextViewDidBeginEditing:self];
if (!self.showPlaceholderWhenFocussed && ![_internalTextView hasText])
_placeholderView.hidden = true;
}
- (void)textViewDidEndEditing:(UITextView *)__unused textView
{
id<HPGrowingTextViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(growingTextViewDidEndEditing:)])
[delegate growingTextViewDidEndEditing:self];
if (!self.showPlaceholderWhenFocussed)
_placeholderView.hidden = [_internalTextView hasText];
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)atext
{
id<HPGrowingTextViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(growingTextViewEnabled:)]) {
if (![delegate growingTextViewEnabled:self]) {
return false;
}
}
if (![textView hasText] && [atext isEqualToString:@""])
return NO;
if ([atext isEqualToString:@"\n"])
{
id<HPGrowingTextViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(growingTextViewShouldReturn:)])
return (BOOL)[delegate performSelector:@selector(growingTextViewShouldReturn:) withObject:self];
}
if (atext.length == 0) {
if (range.location != 0 && range.length == 1) {
NSAttributedString *string = self.attributedText;
if ([string attributesAtIndex:range.location effectiveRange:nil][NSAttachmentAttributeName] != nil) {
NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string];
[mutableString replaceCharactersInRange:NSMakeRange(range.location - 1, 1) withString:@""];
NSRange badRange = [mutableString.string rangeOfString:@"\uFFFC"];
if (badRange.location != NSNotFound)
[mutableString replaceCharactersInRange:badRange withString:@""];
self.attributedText = mutableString;
return false;
}
}
} else if (range.length == 0) {
NSAttributedString *string = self.attributedText;
if (range.location != 0 && [string attributesAtIndex:range.location - 1 effectiveRange:nil][NSAttachmentAttributeName] != nil) {
NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string];
[mutableString replaceCharactersInRange:NSMakeRange(range.location - 1, 0) withString:atext];
NSRange badRange = [mutableString.string rangeOfString:@"\uFFFC"];
if (badRange.location != NSNotFound)
[mutableString replaceCharactersInRange:badRange withString:@""];
self.attributedText = mutableString;
return false;
}
}
return true;
}
- (void)keyCommandPressed:(UIKeyCommand *)keyCommand
{
id<HPGrowingTextViewDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(growingTextView:receivedReturnKeyCommandWithModifierFlags:)])
[delegate growingTextView:self receivedReturnKeyCommandWithModifierFlags:keyCommand.modifierFlags];
}
- (NSArray *)keyCommands
{
if (!self.receiveKeyCommands)
return nil;
return @
[
[UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(keyCommandPressed:)],
[UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:UIKeyModifierAlternate action:@selector(keyCommandPressed:)]
];
}
- (void)refreshAttributes {
if (iosMajorVersion() < 7) {
return;
}
self.internalTextView.typingAttributes = [self defaultAttributes];
NSAttributedString *string = self.attributedText;
if (string.length == 0) {
return;
}
CGPoint contentOffset = _internalTextView.contentOffset;
[_internalTextView setScrollEnabled:false];
_internalTextView.disableContentOffsetAnimation = true;
_internalTextView.freezeContentOffset = true;
//NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string];
//[mutableString removeAttribute:NSForegroundColorAttributeName range:NSMakeRange(0, string.length)];
[_internalTextView.textStorage removeAttribute:NSForegroundColorAttributeName range:NSMakeRange(0, string.length)];
if (_intrinsicTextColor != nil) {
//[mutableString addAttribute:NSForegroundColorAttributeName value:_intrinsicTextColor == nil ? [UIColor blackColor] : _intrinsicTextColor range:NSMakeRange(0, string.length)];
[_internalTextView.textStorage addAttribute:NSForegroundColorAttributeName value:_intrinsicTextColor == nil ? [UIColor blackColor] : _intrinsicTextColor range:NSMakeRange(0, string.length)];
}
__block NSMutableArray<TGInputTextTagAndRange *> *inputTextTags = [[NSMutableArray alloc] init];
[string enumerateAttribute:TGMentionUidAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(__unused id value, NSRange range, __unused BOOL *stop) {
if ([value isKindOfClass:[TGInputTextTag class]]) {
[inputTextTags addObject:[[TGInputTextTagAndRange alloc] initWithTag:value range:range]];
}
}];
[string enumerateAttribute:TGMentionBoldAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(__unused id value, NSRange range, __unused BOOL *stop) {
if ([value isKindOfClass:[TGInputTextTag class]]) {
[inputTextTags addObject:[[TGInputTextTagAndRange alloc] initWithTag:value range:range]];
}
}];
if (inputTextTags != nil) {
/*if (mutableString == nil) {
mutableString = [[NSMutableAttributedString alloc] initWithAttributedString:string];
}*/
static NSCharacterSet *alphanumericSet = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
alphanumericSet = [NSCharacterSet alphanumericCharacterSet];
});
NSMutableSet<NSNumber *> *removeTags = [[NSMutableSet alloc] init];
for (NSInteger i = 0; i < ((NSInteger)inputTextTags.count); i++) {
TGInputTextTagAndRange *tagAndRange = inputTextTags[i];
if ([removeTags containsObject:@(tagAndRange.tag.uniqueId)]) {
[inputTextTags removeObjectAtIndex:i];
//[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
[_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
i--;
} else {
NSInteger j = tagAndRange.range.location;
while (j < (NSInteger)(tagAndRange.range.location + tagAndRange.range.length)) {
unichar c = [string.string characterAtIndex:j];
if (c != ' ') {
break;
}
j++;
}
if (j != (NSInteger)tagAndRange.range.location) {
NSRange updatedRange = NSMakeRange(j, tagAndRange.range.location + tagAndRange.range.length - j);
//[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
[_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
//[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
[_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange];
i--;
} else {
NSInteger j = tagAndRange.range.location;
while (j >= 0) {
unichar c = [string.string characterAtIndex:j];
if (![alphanumericSet characterIsMember:c]) {
break;
}
j--;
}
j++;
if (j < ((NSInteger)tagAndRange.range.location)) {
NSRange updatedRange = NSMakeRange(j, tagAndRange.range.location + tagAndRange.range.length - j);
//[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
[_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
//[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
[_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange];
i--;
} else {
TGInputTextTagAndRange *nextTagAndRange = nil;
if (i != ((NSInteger)inputTextTags.count) - 1) {
nextTagAndRange = inputTextTags[i + 1];
}
if (nextTagAndRange == nil || nextTagAndRange.tag.uniqueId != tagAndRange.tag.uniqueId) {
NSInteger candidateStart = tagAndRange.range.location + tagAndRange.range.length;
NSInteger candidateEnd = nextTagAndRange == nil ? string.length : nextTagAndRange.range.location;
NSInteger j = candidateStart;
while (j < candidateEnd) {
unichar c = [string.string characterAtIndex:j];
static NSCharacterSet *alphanumericSet = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
alphanumericSet = [NSCharacterSet alphanumericCharacterSet];
});
if (![alphanumericSet characterIsMember:c]) {
break;
}
j++;
}
if (j == candidateStart) {
[removeTags addObject:@(tagAndRange.tag.uniqueId)];
//[mutableString addAttribute:NSForegroundColorAttributeName value:TGAccentColor() range:tagAndRange.range];
[_internalTextView.textStorage addAttribute:NSForegroundColorAttributeName value:self.accentColor range:tagAndRange.range];
} else {
//[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
[_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
NSRange updatedRange = NSMakeRange(tagAndRange.range.location, j - tagAndRange.range.location);
//[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
[_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange];
i--;
}
} else {
NSInteger candidateStart = tagAndRange.range.location + tagAndRange.range.length;
NSInteger candidateEnd = nextTagAndRange.range.location;
NSInteger j = candidateStart;
while (j < candidateEnd) {
unichar c = [string.string characterAtIndex:j];
if (![alphanumericSet characterIsMember:c] && c != ' ') {
break;
}
j++;
}
if (j == candidateEnd) {
//[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
[_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
//[mutableString removeAttribute:TGMentionUidAttributeName range:nextTagAndRange.range];
[_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:nextTagAndRange.range];
NSRange updatedRange = NSMakeRange(tagAndRange.range.location, nextTagAndRange.range.location + nextTagAndRange.range.length - tagAndRange.range.location);
//[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
[_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange];
[inputTextTags removeObjectAtIndex:i + 1];
i--;
} else if (j != candidateStart) {
//[mutableString removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
[_internalTextView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range];
NSRange updatedRange = NSMakeRange(tagAndRange.range.location, j - tagAndRange.range.location);
//[mutableString addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
[_internalTextView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange];
inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange];
i--;
} else {
[removeTags addObject:@(tagAndRange.tag.uniqueId)];
//[mutableString addAttribute:NSForegroundColorAttributeName value:TGAccentColor() range:tagAndRange.range];
[_internalTextView.textStorage addAttribute:NSForegroundColorAttributeName value:self.accentColor range:tagAndRange.range];
}
}
}
}
}
}
}
_internalTextView.freezeContentOffset = false;
[_internalTextView setContentOffset:contentOffset];
_internalTextView.disableContentOffsetAnimation = false;
[_internalTextView setScrollEnabled:true];
/*if (mutableString != nil && ![mutableString isEqualToAttributedString:_internalTextView.attributedText])*/ {
/*[_internalTextView.textStorage removeAttribute:NSForegroundColorAttributeName range:NSMakeRange(0, string.length)];
[_internalTextView.textStorage addAttribute:NSForegroundColorAttributeName value:TGAccentColor() range:NSMakeRange(string.length - 1, 1)];
return;
UITextRange *previousRange = [_internalTextView selectedTextRange];
UITextPosition *selStartPos = previousRange.start;
NSInteger previousIdx = [_internalTextView offsetFromPosition:_internalTextView.beginningOfDocument toPosition:selStartPos];
_internalTextView.attributedText = mutableString;
UITextPosition *textPosition = [_internalTextView positionFromPosition:_internalTextView.beginningOfDocument offset:MIN(previousIdx, (NSInteger)mutableString.length)];
[_internalTextView setSelectedTextRange:[_internalTextView textRangeFromPosition:textPosition toPosition:textPosition]];*/
}
}
- (NSString *)textWithEntities:(__autoreleasing NSArray<TGMessageEntity *> ** _Nullable)entities {
NSAttributedString *string = self.attributedText;
NSMutableArray *result = [[NSMutableArray alloc] init];
[string enumerateAttribute:TGMentionUidAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(__unused id value, NSRange range, __unused BOOL *stop) {
if ([value isKindOfClass:[TGInputTextTag class]]) {
[result addObject:[[TGMessageEntityMentionName alloc] initWithRange:range userId:[((TGInputTextTag *)value).attachment intValue]]];
}
}];
if (iosMajorVersion() >= 7) {
[string enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
NSString *fontDescription = font.description;
if ([fontDescription rangeOfString:@"font-weight: bold"].location != NSNotFound) {
[result addObject:[[TGMessageEntityBold alloc] initWithRange:range]];
} else if ([fontDescription rangeOfString:@"font-style: italic"].location != NSNotFound) {
[result addObject:[[TGMessageEntityItalic alloc] initWithRange:range]];
}
}];
}
if (entities) {
*entities = result;
}
return string.string;
}
+ (void)replaceMention:(NSString *)mention inputField:(HPGrowingTextView *)inputField username:(bool)username userId:(int32_t)userId
{
NSString *replacementText = [mention stringByAppendingString:@" "];
NSMutableAttributedString *text = inputField.internalTextView.attributedText == nil ? [[NSMutableAttributedString alloc] init] : [[NSMutableAttributedString alloc] initWithAttributedString:inputField.internalTextView.attributedText];
UITextRange *selRange = inputField.internalTextView.selectedTextRange;
UITextPosition *selStartPos = selRange.start;
NSInteger idx = [inputField.internalTextView offsetFromPosition:inputField.internalTextView.beginningOfDocument toPosition:selStartPos];
idx--;
NSRange candidateMentionRange = NSMakeRange(NSNotFound, 0);
if (idx >= 0 && idx < (int)text.length)
{
for (NSInteger i = idx; i >= 0; i--)
{
unichar c = [text.string characterAtIndex:i];
if (c == '@')
{
if (i == idx)
candidateMentionRange = NSMakeRange(i + 1, 0);
else
candidateMentionRange = NSMakeRange(i + 1, idx - i);
break;
}
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'))
break;
}
}
if (candidateMentionRange.location != NSNotFound)
{
if (!username) {
candidateMentionRange.location -= 1;
candidateMentionRange.length += 1;
[text replaceCharactersInRange:candidateMentionRange withString:replacementText];
static int64_t nextId = 0;
nextId++;
[text addAttributes:@{TGMentionUidAttributeName: [[TGInputTextTag alloc] initWithUniqueId:nextId left:true attachment:@(userId)]} range:NSMakeRange(candidateMentionRange.location, replacementText.length - 1)];
} else {
[text replaceCharactersInRange:candidateMentionRange withString:replacementText];
}
[inputField setAttributedText:text];
UITextPosition *textPosition = [inputField.internalTextView positionFromPosition:inputField.internalTextView.beginningOfDocument offset:candidateMentionRange.location + replacementText.length];
[inputField.internalTextView setSelectedTextRange:[inputField.internalTextView textRangeFromPosition:textPosition toPosition:textPosition]];
}
}
+ (void)replaceHashtag:(NSString *)hashtag inputField:(HPGrowingTextView *)inputField
{
if (inputField.attributedText == nil) {
return;
}
static NSCharacterSet *characterSet = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
characterSet = [NSCharacterSet alphanumericCharacterSet];
});
NSString *replacementText = [hashtag stringByAppendingString:@" "];
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithAttributedString:inputField.attributedText];
UITextRange *selRange = inputField.internalTextView.selectedTextRange;
UITextPosition *selStartPos = selRange.start;
NSInteger idx = [inputField.internalTextView offsetFromPosition:inputField.internalTextView.beginningOfDocument toPosition:selStartPos];
idx--;
NSRange candidateHashtagRange = NSMakeRange(NSNotFound, 0);
NSString *string = text.string;
if (idx >= 0 && idx < (int)text.length)
{
for (NSInteger i = idx; i >= 0; i--)
{
unichar c = [string characterAtIndex:i];
if (c == '#')
{
if (i == idx)
candidateHashtagRange = NSMakeRange(i + 1, 0);
else
candidateHashtagRange = NSMakeRange(i + 1, idx - i);
break;
}
if (c == ' ' || (![characterSet characterIsMember:c] && c != '_'))
break;
}
}
if (candidateHashtagRange.location != NSNotFound)
{
[text replaceCharactersInRange:candidateHashtagRange withString:replacementText];
[inputField setAttributedText:text];
UITextPosition *textPosition = [inputField.internalTextView positionFromPosition:inputField.internalTextView.beginningOfDocument offset:candidateHashtagRange.location + replacementText.length];
[inputField.internalTextView setSelectedTextRange:[inputField.internalTextView textRangeFromPosition:textPosition toPosition:textPosition]];
}
}
@end