-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/make-emojis-larger-in-other-message-contexts #15611
Fix/make-emojis-larger-in-other-message-contexts #15611
Conversation
@roryabraham @parasharrajat One of you needs to copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
src/styles/styles/styles.android.js
Outdated
}, | ||
}; | ||
|
||
export default {...baseStyles, ...styles}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since baseStyles
is a very large object and shouldn't be imported anywhere else, I'd recommend using Object.assign
for better performance:
export default Object.assign(baseStyles, styles);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I used lodash assign method as required by the app standards
export default _.assign(baseStyles, styles);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did you forget to push this change?
src/styles/styles/styles.ios.js
Outdated
}, | ||
}; | ||
|
||
export default {...baseStyles, ...styles}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export default {...baseStyles, ...styles}; | |
export default Object.assign(baseStyles, styles); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I used lodash assign method as required by the app standards
export default _.assign(baseStyles, styles);
src/libs/EmojiUtils.js
Outdated
return false; | ||
} | ||
const trimmedMessage = Str.replaceAll(message.replace(/ /g, ''), '\n', ''); | ||
return Boolean(trimmedMessage.match(CONST.REGEX.EMOJIS)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return Boolean(trimmedMessage.match(CONST.REGEX.EMOJIS)); | |
return CONST.REGEX.EMOJIS.test(trimmedMessage); |
@@ -806,6 +806,7 @@ const CONST = { | |||
|
|||
// eslint-disable-next-line max-len, no-misleading-character-class | |||
EMOJIS: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, | |||
EMOJI_SURROGATE: /(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])/, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain what this regex represents? I was not able to figure it out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the algorithm, we were expected to capture emojis in a text, though while doing this we need to be careful when we capture emojis that are surrogate pairs.
Surrogate pair Emojis are emojis that can be made up of multiple values that are overlayed on each other to get the final displayed representation.
An example of this is the cloud face emoji made up of 2 different emoji (face emoji and cloud emoji) combined together.
If we split the cloud face emoji we would end up with both separate emojis, so from our algorithm, we need to identify these sorts of surrogate pairs and have them combined back together to maintain the original emoji
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks for that explanation.
Maybe what we should do then is:
- Update the emoji regex to capture chunks of 1 or more emojis
- For each chunk containing emojis, use the emoji surrogate regex to capture and "squash" surrogate pairs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also we should include some comments to explain what this regex is about
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Update the emoji regex to capture chunks of 1 or more emojis
- How do I do this from your implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the d flag works for the web but not on android and ios platforms as they both crash
Have logs for this crash? Could this be because the
d
flag is not implemented in the Hermes JS engine, or something else?Edit: Yep, it seems that the
hasIndices
flag is not implemented in Hermes: https://github.com/facebook/hermes/blob/417190297242f14fed540378390d28926c2a7f7e/include/hermes/Regex/RegexTypes.h#L213-L343. Shoot ... did not see that coming.Edit 2: Created facebook/hermes#932, but we need to decide if we should HOLD this PR on that or not
the d flag works for the web but not on android and ios platforms as they both crash
Have logs for this crash? Could this be because the
d
flag is not implemented in the Hermes JS engine, or something else?Edit: Yep, it seems that the
hasIndices
flag is not implemented in Hermes: https://github.com/facebook/hermes/blob/417190297242f14fed540378390d28926c2a7f7e/include/hermes/Regex/RegexTypes.h#L213-L343. Shoot ... did not see that coming.Edit 2: Created facebook/hermes#932, but we need to decide if we should HOLD this PR on that or not
@roryabraham @parasharrajat this would prolong this task by extra few days should we try to fix this regex issue.
I would suggest we go with the former implementation which worked perfectly avoiding the need for this regex d
fix also if it helps I would put a comment on it explaining every step of the algorithm and also remove all the other functions that aren't needed.
/**
* Get all the emojis in the message
* @param {String} text
* @returns {Array}
*/
function getAllEmojiFromText(text) {
// return an empty array when no text is passed
if (!text) {
return [];
}
// Unicode Character 'ZERO WIDTH JOINER' (U+200D) is usually used to join surrogate pair together without breaking the emoji
const zeroWidthJoiner = '\u200D'; // https://codepoints.net/U+200D?lang=en
const splittedMessage = text.split('');
const result = [];
let wordHolder = ''; // word counter
let emojiHolder = ''; // emoji counter
const setResult = (word, isEmoji = false) => {
// for some weird reason javascript sees the empty string `"` as a word with `length = 1`
// this is caused after splitting the text empty spaces are added to both the start and the end of all emojis and text
// given the empty space is close to a text then its length is counted as 0
// while if it's before or after an emoji then it's counted as 1, so we remove the word where word.length equals 1
// NOTE: this does not affect a single character element example typing `[i | J]` cause after splitting its empty word.length is calculated as 0
if (!isEmoji && word.length === 1) {
return;
}
result.push({text: word, isEmoji});
};
_.forEach(splittedMessage, (word, index) => {
if (CONST.REGEX.EMOJI_SURROGATE.test(word) || word === zeroWidthJoiner) {
setResult(wordHolder);
wordHolder = '';
emojiHolder += word;
} else {
setResult(emojiHolder, true);
emojiHolder = '';
wordHolder += word;
}
if (index === splittedMessage.length - 1) {
setResult(emojiHolder, true);
setResult(wordHolder);
}
});
// remove none text characters like '' only return where text is a word or white space ' '
return _.filter(result, res => res.text);
}
/**
* Validates that this message contains has emojis
*
* @param {String} message
* @returns {Boolean}
*/
function hasEmojis(message) {
const splitText = getAllEmojiFromText(message);
return _.find(splitText, chunk => chunk.isEmoji) !== undefined;
}
/**
* Validates that this message contains only emojis
*
* @param {String} message
* @returns {Boolean}
*/
function containsOnlyEmojis(message) {
const splitText = getAllEmojiFromText(message);
return _.every(splitText, chunk => chunk.isEmoji);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a number of other changes I want to see included in this PR.
Unit Test Coverage
EmojiUtils
desperately needs some unit test coverage. That's almost always a good idea when we are dealing with complex regexes. Furthermore, it's probably a good place to start to establish a baseline before making the other changes listed here.
Unused Method
Next, I noticed that EmojiUtils.trimEmojiUnicode
is unused, so we should remove it
Refactoring EmojiUtils
Third, I noticed some inefficiencies and unnecessarily complex code in the following three methods:
containsOnlyEmoji
hasEmojis
getAllEmojisFromText
I think we should refactor these methods such that the string is only traversed by a regex exactly once when any one of the three methods is called.
I've done some research, and I think this should be possible in a few steps.
Step 1
Refactor CONST.REGEX.EMOJIS such that it will match not just a single emoji at a time, but 1 or more. This means that if we have an input string like so:
Hello world 🙂🙂🙂 what a great day ☀️
const input = 'Hello world U+1F600U+1F600U+1F600 what a great day U+2600';
then we do this:
input.split(CONST.REGEX.EMOJIS);
We would get a result like this:
['Hello world ', ':slightly_smiling_face::slightly_smiling_face::slightly_smiling_face:', ' what a great day ', ':sunny:']
result = ['Hello world ', 'U+1F600U+1F600U+1F600', ' what a great day ', 'U+2600']
Furthermore, it should ignore whitespace or 0-length characters if they are surrounded by emoji. So doing a split on this input:
const input = 'Hello world U+1F600 U+1F600 U+1F600 what a great day U+2600'
would produce this output:
result = ['Hello world ', 'U+1F600 U+1F600 U+1F600', ' what a great day ', 'U+2600']
Step 2
- Add the d flag to
CONST.REGEX.EMOJIS
such that the result of a regular expression match should contain the start and end indices of the substrings of each capture group. - Wrap the regex in a capture group such that the chunks of 1 or more potentially-whitespace-separated emojis are captured.
Step 3
Refactor EmojiUtils.getAllEmojisFromText
so that it works something like this (this hasn't been properly tested, but should be enough to give you the idea).
- note: when the regex has the global flag then it becomes stateful where
exec
just gives you one match at a time. - note: while
exec
is used multiple times, the entire string is only traversed once.
function getAllEmojiFromText(text) {
if (!text) {
return [];
}
const splitText = [];
let reResult;
let lastMatchIndexEnd = 0;
do {
// Look for an emoji chunk in the string
reResult = CONST.REGEX.EMOJIS.exec(text);
// If we reached the end of the string and it wasn't included in a previous match
// the chunk between the end of the last match and the end of the string is plain text
if (reResult === null && lastMatchIndexEnd !== text.length - 1) {
splitText.push({
text: text.slice(lastMatchIndexEnd, text.length - 1),
isEmoji: false,
})
continue;
}
const matchIndexStart = reResult.indices[0][0];
const matchIndexEnd = reResult.indices[0][1];
// The chunk between the end of the last match and the start of the new one is plain-text
splitText.push({
text: text.slice(lastMatchIndexEnd, matchIndexStart),
isEmoji: false,
});
// Everything captured by the regex itself is emoji + whitespace
splitText.push({
text: text.slice(matchIndexStart, matchIndexEnd),
isEmoji: true,
});
lastMatchIndexEnd = matchIndexEnd;
} while (reResult !== null);
return splitText;
}
Step 4
Refactor EmojiUtils.hasEmojis
like so:
function hasEmojis(message) {
const splitText = getAllEmojiFromText(message);
return _.find(splitText, chunk => chunk.isEmoji) !== undefined;
}
Step 5
Refactor EmojiUtils.containsOnlyEmojis
like so:
function containsOnlyEmojis(message) {
const splitText = getAllEmojiFromText(message);
return _.every(splitText, chunk => chunk.isEmoji);
}
@roryabraham from my test with your solution adding the EMOJIS: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/dgu, Also, the solution does not satisfy surrogate pair emojis, more context on this here #15611 (comment) |
Have logs for this crash? Could this be because the Edit: Yep, it seems that the Edit 2: Created facebook/hermes#932, but we need to decide if we should HOLD this PR on that or not |
@fabioh8010 I just ran 262 against latest main and it reported no errors. It reported 19274 passing tests, so it seems like for some reason you are running more tests. |
Maybe we should continue discussion on this topic either in an upstream draft PR or in facebook/hermes#932 ? |
@roryabraham @fbmal7 Yes, it's better. I'll create a draft PR tomorrow and will let you know, so we can have continue discussions there. |
I published a Draft PR here -> facebook/hermes#968 |
Update: PR is being reviewed, I left some questions there after the first review and I'll address all of them once I get feedback. |
Update: PR reviewed and some changes made, also I made it ready to review. |
Update: PR reviewed again and I need to make some more changes to keep the implementation closer to ECMAScript specifications. I'll be OOO for some days and will return on May 4th to start working on these changes in the following days. |
Got some issues Held on this. How is the review going? |
Hi @twisterdotcom , I addressed the last review comments on Friday and I'm now waiting for his feedback. Latest update -> facebook/hermes#968 (comment) |
Update: Addressed more requested changes on the PR, waiting for another review now. |
Is there any reason why this is on HOLD? |
Yes, @pecanoro it's on HOLD for a new regex feature in the Hermes JS engine: facebook/hermes#968 More context in this comment |
Update: Looks like we are coming to an end in the Hermes PR 🎉 Meta's team is discussing how to merge those changes. |
@fabioh8010 yessss! |
Update: Still waiting for Meta's team decision about how to merge the Hermes PR. |
Don't know much context of the PR but if you are looking for the Regex to get all the emojis. Check this comment: |
@jeet-dhandha The Hermes engine doesn't support unicode property escapes -> https://hermesengine.dev/docs/regexp#supported-syntax So this type of regex will crash on mobile platforms. |
Update: Hermes PR is expected to land on RN 0.73 -> facebook/hermes#968 (comment) |
This is blocked on the RN 0.73 release. They just published 0.73-rc.3 a few days ago |
It seems RN 0.73 has finally been released! |
Yes, and we're upgrading E/App to 0.73.1 in #31558 |
Just wondering, can we finally remove the HOLD in this PR? |
Yep, I think we can lift the HOLD here 🎉 |
Who is responsible for this now? @avijeetpandey? |
Not sure, asked in slack: https://expensify.enterprise.slack.com/archives/C03UK30EA1Z/p1706080610772919 |
Closing this out per this comment |
HOLD on facebook/hermes#932
Details
This solution covers several related scenarios:
Scenario 1
Action Performed:
Add an emoji with other plain text characters in a message in the composer.
Expected Result:
The emoji should be larger (19pt) compared to the default size for text (15pt) in the composer. The emoji and text should appear vertically centered with respect to each other, such that the baseline of the emoji is below the plain text and the middle of the text vertically aligns with the middle of the emoji (like the right-hand-side of #4114 (comment).
Scenario 2
Action Performed:
Send a message with emojis and text.
Expected Result:
The emoji should be larger (19pt) compared to the default size for text (15pt) in the chat history. The emoji and text should appear vertically centered with respect to each other, such that the baseline of the emoji is below the plain text and the middle of the text vertically aligns with the middle of the emoji (like the right-hand-side of #4114 (comment)
Scenario 3
Action Performed
Draft a message (of any length) with only an emoji.
Expected Result:
The emoji should appear very large (27pt) in the composer.
Scenario 4
Action Performed
Send a message (of any length) with only emoji.
Expected Result:
The emoji should appear very large (27pt) in the report history.
Scenario 5
Action Performed
Go to
Settings
-> Profile and add emoji(s) to your last name 😎Expected Result:
The emoji should appear larger (19pt) than the rest of the text (15pt) in the text input. The emoji and text should appear vertically centered with respect to each other, such that the baseline of the emoji is below the plain text and the middle of the text vertically aligns with the middle of the emoji (like the right-hand-side of #4114 (comment)
Scenario 6
Action Performed
Settings
->Profile
and add emoji(s) to your last name 😎.Settings
RHP and look at your name:Expected Result:
The emoji should appear larger (21pt) than the rest of the text (17pt) in the text input. The emoji and text should appear vertically centered with respect to each other, such that the baseline of the emoji is below the plain text and the middle of the text vertically aligns with the middle of the emoji (like the right-hand-side of #4114 (comment)
Scenario 7
Action Performed
Settings
->Profile
and add emoji(s) to your last name 😎.Expected Result:
The emoji should appear larger (19pt) than the rest of the test (15pt). The emoji and text should appear vertically centered with respect to each other, such that the baseline of the emoji is below the plain text and the middle of the text vertically aligns with the middle of the emoji (like the right-hand-side of #4114 (comment)
Fixed Issues
PROPOSAL: #4114
Tests
Offline tests
QA Steps
PR Author Checklist
### Fixed Issues
section aboveTests
sectionOffline steps
sectionQA steps
sectiontoggleReport
and notonIconClick
)src/languages/*
files and using the translation methodWaiting for Copy
label for a copy review on the original GH to get the correct copy.STYLE.md
) were followedAvatar
, I verified the components usingAvatar
are working as expected)/** comment above it */
this
are necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);
ifthis.submit
is never passed to a component event handler likeonClick
)StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG
)Avatar
is modified, I verified thatAvatar
is working as expected in all cases)ScrollView
component to make it scrollable when more elements are added to the page.Screenshots/Videos
Chrome Web
Chrome.Web.Recording.mov
Safari Web
Safari.Web.Recording.mov
Mobile Web - Chrome
Mobile.Web.-.Chrome.mov
Mobile Web - Safari
Mobile.Web.-.Safari.mp4
Desktop
Desktop.mov
iOS
iOS.mp4
Android
Android.mov