Skip to content

Commit 4073a47

Browse files
authored
feat: make iOS mentions work with a single space (#289)
<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please follow the template so that the reviewers can easily understand what the code changes affect --> # Summary This PR makes iOS mentions tolerate a single space. Code responsible for detecting a currently edited mention would just take the current word into the account. Now it works by trying to search for one more word before the current one if needed. This way all of the following scenarios will be detected as editable mention: - current word with a mention indicator - a space character, but previous word has a mention indicator - current word with no mention indicator but previous word has one Effectively these scenarios are enough to achieve space tolerating mentions. ## Test Plan Open iOS example app and play with mentions and spaces. ## Screenshots / Videos https://github.com/user-attachments/assets/80b53b4d-509a-4d43-8a33-53e911e27132 ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | Android | ❌ |
1 parent fa2109e commit 4073a47

2 files changed

Lines changed: 127 additions & 32 deletions

File tree

ios/EnrichedTextInputView.mm

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,7 +1035,12 @@ - (void)manageSelectionBasedChanges {
10351035
MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
10361036
if(mentionStyleClass != nullptr) {
10371037
[mentionStyleClass manageMentionTypingAttributes];
1038-
[mentionStyleClass manageMentionEditing];
1038+
1039+
// mention editing runs if only a selection was done (no text change)
1040+
// otherwise we would double-emit with a second call in the anyTextMayHaveBeenModified method
1041+
if([_recentlyEmittedString isEqualToString:[textView.textStorage.string copy]]) {
1042+
[mentionStyleClass manageMentionEditing];
1043+
}
10391044
}
10401045

10411046
// typing attributes for empty lines selection reset
@@ -1112,10 +1117,11 @@ - (void)anyTextMayHaveBeenModified {
11121117
[h3Style handleImproperHeadings];
11131118
}
11141119

1115-
// mentions removal management
1120+
// mentions management: removal and editing
11161121
MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
11171122
if(mentionStyleClass != nullptr) {
11181123
[mentionStyleClass handleExistingMentions];
1124+
[mentionStyleClass manageMentionEditing];
11191125
}
11201126

11211127
// placholder management

ios/styles/MentionStyle.mm

Lines changed: 119 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -299,45 +299,24 @@ - (void)manageMentionEditing {
299299
return;
300300
}
301301

302-
// get the current word if it exists
303-
// we can be using current word only thanks to the fact that ongoing mentions are always one word (in contrast to ready, added mentions)
304-
NSDictionary *currentWord = [WordsUtils getCurrentWord:_input->textView.textStorage.string range:_input->textView.selectedRange];
305-
if(currentWord == nullptr) {
302+
// get the text (and its range) that could be an editable mention
303+
NSArray *mentionCandidate = [self getMentionCandidate];
304+
if(mentionCandidate == nullptr) {
306305
[self removeActiveMentionRange];
307306
return;
308307
}
308+
NSString *candidateText = mentionCandidate[0];
309+
NSRange candidateRange = [(NSValue *)mentionCandidate[1] rangeValue];
309310

310-
// get word properties
311-
NSString *wordText = (NSString *)[currentWord objectForKey:@"word"];
312-
NSValue *wordRangeValue = (NSValue *)[currentWord objectForKey:@"range"];
313-
if(wordText == nullptr || wordRangeValue == nullptr) {
314-
[self removeActiveMentionRange];
315-
return;
316-
}
317-
NSRange wordRange = [wordRangeValue rangeValue];
318-
319-
// check for mentionIndicators - no sign of them means we shouldn't be editing a mention
320-
unichar firstChar = [wordText characterAtIndex:0];
321-
if(![[_input->config mentionIndicators] containsObject: @(firstChar)]) {
322-
[self removeActiveMentionRange];
323-
return;
324-
}
325-
326-
// check for existing mentions - we don't edit them
327-
if([self detectStyle:wordRange]) {
328-
[self removeActiveMentionRange];
329-
return;
330-
}
331-
332-
// get style classes that the mention shouldn't be recognized in
311+
// get style classes that the mention shouldn't be recognized in, together with other mentions
333312
NSArray *conflicts = _input->conflictingStyles[@([MentionStyle getStyleType])];
334313
NSArray *blocks = _input->blockingStyles[@([MentionStyle getStyleType])];
335-
NSArray *allConflicts = [conflicts arrayByAddingObjectsFromArray:blocks];
314+
NSArray *allConflicts = [[conflicts arrayByAddingObjectsFromArray:blocks] arrayByAddingObject:@([MentionStyle getStyleType])];
336315
BOOL conflictingStyle = NO;
337316

338317
for(NSNumber *styleType in allConflicts) {
339318
id<BaseStyleProtocol> styleClass = _input->stylesDict[styleType];
340-
if(styleClass != nullptr && [styleClass anyOccurence:wordRange]) {
319+
if(styleClass != nullptr && [styleClass anyOccurence:candidateRange]) {
341320
conflictingStyle = YES;
342321
break;
343322
}
@@ -350,7 +329,7 @@ - (void)manageMentionEditing {
350329
}
351330

352331
// everything checks out - we are indeed editing a mention
353-
[self setActiveMentionRange:wordRange text:wordText];
332+
[self setActiveMentionRange:candidateRange text:candidateText];
354333
}
355334

356335
// used to fix mentions' typing attributes
@@ -462,6 +441,116 @@ - (MentionStyleProps *)stylePropsWithParams:(MentionParams *)params {
462441
return [_input->config mentionStylePropsForIndicator:params.indicator];
463442
}
464443

444+
// finds if any word/words around current selection are eligible to be edited as mentions
445+
// since we allow for a single space inside an edited mention, we have take both current and the previous word into account
446+
- (NSArray *)getMentionCandidate {
447+
NSDictionary *currentWord, *previousWord;
448+
NSString *currentWordText, *previousWordText, *finalText;
449+
NSValue *currentWordRange, *previousWordRange;
450+
NSRange finalRange;
451+
452+
// word at the current selection
453+
currentWord = [WordsUtils getCurrentWord:_input->textView.textStorage.string range:_input->textView.selectedRange];
454+
if(currentWord != nullptr ) {
455+
currentWordText = (NSString *)[currentWord objectForKey:@"word"];
456+
currentWordRange = (NSValue *)[currentWord objectForKey:@"range"];
457+
}
458+
459+
if(currentWord != nullptr) {
460+
// current word exists
461+
unichar currentFirstChar = [currentWordText characterAtIndex:0];
462+
463+
if([[_input->config mentionIndicators] containsObject:@(currentFirstChar)]) {
464+
// current word exists and has a mention indicator; no need to check for the previous word
465+
finalText = currentWordText;
466+
finalRange = [currentWordRange rangeValue];
467+
} else {
468+
// current word exists but no traces of mention indicator; get the previous word
469+
470+
NSInteger previousWordSearchLocation = [currentWordRange rangeValue].location - 1;
471+
if(previousWordSearchLocation < 0) {
472+
// previous word can't exist
473+
return nullptr;
474+
}
475+
476+
unichar separatorChar = [_input->textView.textStorage.string characterAtIndex:previousWordSearchLocation];
477+
if(![[NSCharacterSet whitespaceCharacterSet] characterIsMember:separatorChar]) {
478+
// we want to check for the previous word ONLY if the separating character was a space
479+
// newlines don't make it
480+
return nullptr;
481+
}
482+
483+
previousWord = [WordsUtils getCurrentWord:_input->textView.textStorage.string range:NSMakeRange(previousWordSearchLocation, 0)];
484+
485+
if(previousWord != nullptr) {
486+
// previous word exists; get its properties
487+
previousWordText = (NSString *)[previousWord objectForKey:@"word"];
488+
previousWordRange = (NSValue *)[previousWord objectForKey:@"range"];
489+
490+
// check for the mention indicators in the previous word
491+
unichar previousFirstChar = [previousWordText characterAtIndex:0];
492+
493+
if([[_input->config mentionIndicators] containsObject:@(previousFirstChar) ]) {
494+
// previous word has a proper mention indicator: treat both words as an editable mention
495+
finalText = [NSString stringWithFormat:@"%@ %@", previousWordText, currentWordText];
496+
// range length is both words' lengths + 1 for a space between them
497+
finalRange = NSMakeRange(
498+
[previousWordRange rangeValue].location,
499+
[previousWordRange rangeValue].length + [currentWordRange rangeValue].length + 1
500+
);
501+
} else {
502+
// neither current nor previous words have a mention indicator
503+
return nullptr;
504+
}
505+
} else {
506+
// previous word doesn't exist and no mention indicators in the current word
507+
return nullptr;
508+
}
509+
}
510+
} else {
511+
// current word doesn't exist; try getting the previous one
512+
513+
NSInteger previousWordSearchLocation = _input->textView.selectedRange.location - 1;
514+
if(previousWordSearchLocation < 0) {
515+
// previous word can't exist
516+
return nullptr;
517+
}
518+
519+
unichar separatorChar = [_input->textView.textStorage.string characterAtIndex:previousWordSearchLocation];
520+
if(![[NSCharacterSet whitespaceCharacterSet] characterIsMember:separatorChar]) {
521+
// we want to check for the previous word ONLY if the separating character was a space
522+
// newlines don't make it
523+
return nullptr;
524+
}
525+
526+
previousWord = [WordsUtils getCurrentWord:_input->textView.textStorage.string range:NSMakeRange(previousWordSearchLocation, 0)];
527+
528+
if(previousWord != nullptr) {
529+
// previous word exists; get its properties
530+
previousWordText = (NSString *)[previousWord objectForKey:@"word"];
531+
previousWordRange = (NSValue *)[previousWord objectForKey:@"range"];
532+
533+
// check for the mention indicators in the previous word
534+
unichar previousFirstChar = [previousWordText characterAtIndex:0];
535+
536+
if([[_input->config mentionIndicators] containsObject:@(previousFirstChar)]) {
537+
// previous word has a proper mention indicator; treat previous word + a space as a editable mention
538+
finalText = [NSString stringWithFormat:@"%@ ", previousWordText];
539+
// the range length is previous word length + 1 for a space
540+
finalRange = NSMakeRange([previousWordRange rangeValue].location, [previousWordRange rangeValue].length + 1);
541+
} else {
542+
// no current word, previous has no mention indicators
543+
return nullptr;
544+
}
545+
} else {
546+
// no current word, no previous word
547+
return nullptr;
548+
}
549+
}
550+
551+
return @[finalText, [NSValue valueWithRange:finalRange]];
552+
}
553+
465554
// both used for setting the active mention range + indicator and fires proper onMention event
466555
- (void)setActiveMentionRange:(NSRange)range text:(NSString *)text {
467556
NSString *indicatorString = [NSString stringWithFormat:@"%C", [text characterAtIndex:0]];

0 commit comments

Comments
 (0)