Expensify / App

Welcome to New Expensify: a complete re-imagination of financial collaboration, centered around chat. Help us build the next generation of Expensify by sharing feedback and contributing to the code.
https://new.expensify.com
MIT License
2.99k stars 2.5k forks source link

[HOLD for payment 2023-02-15] [$4000] Error message appears when sending 15000 emojis #13988

Closed kavimuru closed 1 year ago

kavimuru commented 1 year ago

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Action Performed:

  1. navigate to any chat
  2. send 15000 emoji to the chat
  3. Compose with 15000 characters and add one more emoji

Expected Result:

Emojis should be sent since it's exactly 15000 and no error message should be. Also after 15000 characters adding an emoji should show the count as 15001

Actual Result:

getting an error Auth CreeateReportAction returned an error In step 3 shows the count as 15002

Workaround:

unknown

Platforms:

Which of our officially supported platforms is this issue occurring on?

Version Number: 1.2.48-2

Reproducible in staging?: y Reproducible in production?: y Email or phone of affected tester (no customers): Logs: https://stackoverflow.com/c/expensify/questions/4856 Notes/Photos/Videos:

Screenshot 2022-12-30 at 9 44 27 PM

https://user-images.githubusercontent.com/43996225/210617277-d1ceed8f-e611-4fb9-8864-95ff573f7c53.mov

https://user-images.githubusercontent.com/43996225/210617296-f6330503-3303-47b3-883b-2b42b891d2e6.mp4

https://user-images.githubusercontent.com/43996225/210617301-bee7c7ba-5c16-41fb-80a1-9602ce9d8994.mp4

Expensify/Expensify Issue URL: Issue reported by: @jayeshmangwani Slack conversation: https://expensify.slack.com/archives/C049HHMV9SM/p1672427549244909

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~01cb872390d8ccc3cf
  • Upwork Job ID: 1611228769941909504
  • Last Price Increase: 2023-01-25
adelekennedy commented 1 year ago

@jayeshmangwani what platform are you getting this error on? This looks like a pretty reproducible bug and one we agreed we should solve

melvin-bot[bot] commented 1 year ago

Job added to Upwork: https://www.upwork.com/jobs/~01cb872390d8ccc3cf

melvin-bot[bot] commented 1 year ago

Current assignee @adelekennedy is eligible for the External assigner, not assigning anyone new.

melvin-bot[bot] commented 1 year ago

Triggered auto assignment to Contributor-plus team member for initial proposal review - @mollfpr (External)

melvin-bot[bot] commented 1 year ago

Triggered auto assignment to @stitesExpensify (External), see https://stackoverflow.com/c/expensify/questions/7972 for more details.

adelekennedy commented 1 year ago

asked clarifying question here before we make this external

jayeshmangwani commented 1 year ago

@jayeshmangwani what platform are you getting this error on?

@adelekennedy desktop

adelekennedy commented 1 year ago

Chrome? I'm trying to complete the checklist above

jayeshmangwani commented 1 year ago

Yes, gettings errors on macOS chrome and macOS desktop

getusha commented 1 year ago

this might be issue from the backend?

adelekennedy commented 1 year ago

@getusha yes! I asked a clarifying question here though I accidentally added 'external' too soon!

mollfpr commented 1 year ago

@adelekennedy I think we are looking for external approach?

melvin-bot[bot] commented 1 year ago

Current assignee @adelekennedy is eligible for the External assigner, not assigning anyone new.

melvin-bot[bot] commented 1 year ago

Current assignee @mollfpr is eligible for the External assigner, not assigning anyone new.

melvin-bot[bot] commented 1 year ago

Current assignee @stitesExpensify is eligible for the External assigner, not assigning anyone new.

adelekennedy commented 1 year ago

moved to external!

priyeshshah11 commented 1 year ago

The issue is that the frontend doesn't correctly count the length of the message, the right length can be calculated by converting the string to bytes and then count it's length.

Also after 15000 characters adding an emoji should show the count as 15001

This is not true as emoji's are made up of several characters

stitesExpensify commented 1 year ago

This is not true as emoji's are made up of several characters

Just to confirm, that also means that there aren't actually 15k emojis right? It's U+1F928 (7 characters) so in this case it would be 15000/7 = 2142 so 2142 emojis would put us at 14999 and we shouldn't be able to add another emoji?

priyeshshah11 commented 1 year ago

This is not true as emoji's are made up of several characters

Just to confirm, that also means that there aren't actually 15k emojis right? It's U+1F928 (7 characters) so in this case it would be 15000/7 = 2142 so 2142 emojis would put us at 14999 and we shouldn't be able to add another emoji?

that's correct, they aren't 15000 emojis. But even 2142 won't always work as it would depend on the emoji's that are in the message, they are not all of the same length.

stitesExpensify commented 1 year ago

Gotcha, so IMO this is not actually a bug then and this is functioning as expected. If we actually want the hard limit to be 15k characters, then adding an emoji does go over that limit so we should not allow that. Would you agree @priyeshshah11 ?

priyeshshah11 commented 1 year ago

Gotcha, so IMO this is not actually a bug then and this is functioning as expected. If we actually want the hard limit to be 15k characters, then adding an emoji does go over that limit so we should not allow that. Would you agree @priyeshshah11 ?

@stitesExpensify No, unfortunately it is still a bug as the UI allows you to send the message with let's say 14999 characters but then you get an error from the backend. So ideally we should fix it by matching the BE & FE on how it calculates the message length.

stitesExpensify commented 1 year ago

ah! Understood, thanks for that. In that case I like your solution. Can you make a proposal including code so that @mollfpr can take a look and we can potentially get you hired for the issue?

tienifr commented 1 year ago

Hi @stitesExpensify, Note 2 in expected result: "Also after 15000 characters adding an emoji should show the count as 15001". We can fix this problem by converting all emojis into 1 character like Twitter (convert all emojis into 2 characters)

If it makes sense to you, we can do like that

Screen Shot 2023-01-10 at 15 43 17

And then on BE side, we also convert all emojis into 1 character and count by character to match to FE side

getusha commented 1 year ago

RCA The varying length of emojis can result in inaccuracies when using the .length property to determine their count, as some emojis may have more than 5 characters.

for example

"πŸ˜‚".length = 2

This is an interesting thing why is it returning 2?

"πŸ˜‚".split("") = ['\uD83D', '\uDE02']

The emoji is constructed by 2 unicodes, that means that's not it's actual length. The backend takes this emojis and count their size as string and currently it's exceeding in amount that's why we're getting the error. The solution is to count the emoji unicodes and update the limit.

Solution

index 1d60d3d8f..00afe09f3 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -467,7 +467,7 @@ class ReportActionCompose extends React.Component {
         const trimmedComment = this.comment.trim();

         // Don't submit empty comments or comments that exceed the character limit
-        if (this.state.isCommentEmpty || trimmedComment.length > CONST.MAX_COMMENT_LENGTH) {
@@ -467,7 +467,7 @@ class ReportActionCompose extends React.Component {
         const trimmedComment = this.comment.trim();

         // Don't submit empty comments or comments that exceed the character limit
-        if (this.state.isCommentEmpty || trimmedComment.length > CONST.MAX_COMMENT_LENGTH) {
+        if (this.state.isCommentEmpty || trimmedComment.length > CONST.MAX_COMMENT_LENGTH || EmojiUtils.emojiWithTextCount(trimmedComment) > CONST.MAX_COMMENT_LENGTH) {
             return '';
         }

@@ -530,7 +530,8 @@ class ReportActionCompose extends React.Component {
         const isComposeDisabled = this.props.isDrawerOpen && this.props.isSmallScreenWidth;
         const isBlockedFromConcierge = ReportUtils.chatIncludesConcierge(this.props.report) && User.isBlockedFromConcierge(this.props.blockedFromConcierge);
         const inputPlaceholder = this.getInputPlaceholder();
-        const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH;
+        const hasExceededMaxCommentLength = EmojiUtils.emojiWithTextCount(this.comment) > CONST.MAX_COMMENT_LENGTH;
+

         return (
             <View style={[
@@ -723,7 +724,7 @@ class ReportActionCompose extends React.Component {
                 >
                     {!this.props.isSmallScreenWidth && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}
                     <ReportTypingIndicator reportID={this.props.reportID} />
-                    <ExceededCommentLength commentLength={this.comment.length} />
+                    <ExceededCommentLength commentLength={EmojiUtils.emojiWithTextCount(this.comment)} />
                 </View>
                 {this.state.isDraggingOver && <ReportDropUI />}
             </View>
index b7fa88aef..34b9dbd79 100644
--- a/src/libs/EmojiUtils.js
+++ b/src/libs/EmojiUtils.js
@@ -245,6 +245,29 @@ function suggestEmojis(text, limit = 5) {
     return [];
 }

+function separateEmoji(text) {
+    // Regular expression for matching emojis
+    const emojiRegex = CONST.REGEX.EMOJIS;
+    // Extract all the emojis from the input text
+    const unicodeEmojis = text.match(emojiRegex) || [];
+    // Split the input text by the emoji regular expression and join the resulting parts
+    let textWithoutEmoji = text.split(emojiRegex).join('');
+    // Remove remaining emojis using the same regular expression
+    textWithoutEmoji = textWithoutEmoji.replace(emojiRegex, '');
+
+    // Return the extracted emojis and text without emojis
+    return { unicodeEmojis, textWithoutEmoji };
+}
+
+function emojiWithTextCount(text) {
+    const {unicodeEmojis, textWithoutEmoji} = separateEmoji(text);
+    const textWithoutEmojiLength = textWithoutEmoji.length;
+    const unicodeEmojiLength = unicodeEmojis.join("").split("").join(" ").length;
-    const totalTextLength = unicodeEmojiLength + textWithoutEmojiLength;
+    const totalTextLength = (unicodeEmojiLength + textWithoutEmojiLength) - (unicodeEmojis.length * .995);
+
+    return round(totalTextLength);
+}
+
 export {
     getDynamicHeaderIndices,
     mergeEmojisWithFrequentlyUsedEmojis,
@@ -253,4 +276,6 @@ export {
     replaceEmojis,
     suggestEmojis,
     trimEmojiUnicode,
+    separateEmoji,
+    emojiWithTextCount
 };

cc @mollfpr

s77rt commented 1 year ago

Proposal

I think what we are looking for here is a grapheme cluster counter that implements the UAX-29 standard.

FE: I found this library that already does that https://github.com/orling/grapheme-splitter (or a fork)

BE: I think we can simply use grapheme_strlen


Edit: I will post a complete proposal for the FE later

priyeshshah11 commented 1 year ago

Proposal

Problem

There are two problems here, one we are calculating the comment length incorrectly and the max length limit is incorrect. After debugging for quite some time I have observed that the actual comment length is being restricted to 10000.

Solution

We should calculate the comment length like below & change the comment characters limit to match the backend (10000).

diff --git a/src/CONST.js b/src/CONST.js
index b8bb1b496..b8d1f40d2 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -811,7 +811,7 @@ const CONST = {
     },

     // Auth limit is 60k for the column but we store edits and other metadata along the html so let's use a lower limit to accommodate for it.
-    MAX_COMMENT_LENGTH: 15000,
+    MAX_COMMENT_LENGTH: 10000,

     FORM_CHARACTER_LIMIT: 50,
     AVATAR_CROP_MODAL: {
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index acd262d2e..58984a5aa 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -427,6 +427,10 @@ class ReportActionCompose extends React.Component {
         }
     }

+    calculateCommentLength(comment) {
+        return (comment.length ? _.reduce(_.map(comment.split(''), character => character.length), (prev, next) => prev + next) : 0);
+    }
+
     /**
      * Listens for keyboard shortcuts and applies the action
      *
@@ -723,7 +727,7 @@ class ReportActionCompose extends React.Component {
                 >
                     {!this.props.isSmallScreenWidth && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}
                     <ReportTypingIndicator reportID={this.props.reportID} />
-                    <ExceededCommentLength commentLength={this.comment.length} />
+                    <ExceededCommentLength commentLength={this.calculateCommentLength(this.comment)} />
                 </View>
                 {this.state.isDraggingOver && <ReportDropUI />}
             </View>

Note: I can also improve the usage of map & reduce by only reduce but currently getting an error with that, will update once it is working

@stitesExpensify @mollfpr

mollfpr commented 1 year ago

Anyone can send me the 15000 emoji to test? πŸ˜…

getusha commented 1 year ago

@mollfpr here we go

🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨🀨
priyeshshah11 commented 1 year ago

@mollfpr @getusha FYI they don't look like 15000 emojis, they are only 5792 emoji and 11584 characters

Screen Shot 2023-01-10 at 11 19 04 PM
getusha commented 1 year ago

@priyeshshah11 got it from here https://expensify.slack.com/archives/C049HHMV9SM/p1672427549244909 the bug report

priyeshshah11 commented 1 year ago

Proposal Update

Updating my proposal from here https://github.com/Expensify/App/issues/13988#issuecomment-1377122892 Only issue here is that the actual comment length is being restricted to 10000 from the BE.

Solution

This is the only change required

diff --git a/src/CONST.js b/src/CONST.js
index b8bb1b496..b8d1f40d2 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -811,7 +811,7 @@ const CONST = {
     },

     // Auth limit is 60k for the column but we store edits and other metadata along the html so let's use a lower limit to accommodate for it.
-    MAX_COMMENT_LENGTH: 15000,
+    MAX_COMMENT_LENGTH: 10000,

     FORM_CHARACTER_LIMIT: 50,
     AVATAR_CROP_MODAL: {

@mollfpr @stitesExpensify

s77rt commented 1 year ago

Proposal (Updated)

diff --git a/package-lock.json b/package-lock.json
index 4bfaec2b24..201cfc34e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,6 +41,7 @@
         "expensify-common": "git+https://github.com/Expensify/expensify-common.git#55c208315e04246b548a4009d35466c100d9de7c",
         "fbjs": "^3.0.2",
         "file-loader": "^6.0.0",
+        "graphemer": "^1.4.0",
         "html-entities": "^1.3.1",
         "htmlparser2": "^7.2.0",
         "localforage": "^1.10.0",
@@ -25393,6 +25394,11 @@
       "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==",
       "dev": true
     },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
+    },
     "node_modules/growly": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
@@ -62043,6 +62049,11 @@
       "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==",
       "dev": true
     },
+    "graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
+    },
     "growly": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
diff --git a/package.json b/package.json
index 5a642f45a3..7121892753 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
     "expensify-common": "git+https://github.com/Expensify/expensify-common.git#55c208315e04246b548a4009d35466c100d9de7c",
     "fbjs": "^3.0.2",
     "file-loader": "^6.0.0",
+    "graphemer": "^1.4.0",
     "html-entities": "^1.3.1",
     "htmlparser2": "^7.2.0",
     "localforage": "^1.10.0",
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index acd262d2e7..df45d755eb 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -8,6 +8,7 @@ import {
 import _ from 'underscore';
 import lodashGet from 'lodash/get';
 import {withOnyx} from 'react-native-onyx';
+import Graphemer from 'graphemer';
 import lodashIntersection from 'lodash/intersection';
 import styles from '../../../styles/styles';
 import themeColors from '../../../styles/themes/default';
@@ -138,6 +139,8 @@ class ReportActionCompose extends React.Component {
         this.comment = props.comment;
         this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus();

+        this.graphemer = new Graphemer();
+
         this.state = {
             isFocused: this.shouldFocusInputOnScreenFocus && !this.props.modal.isVisible && !this.props.modal.willAlertModalBecomeVisible,
             isFullComposerAvailable: props.isComposerFullSize,
@@ -467,7 +470,7 @@ class ReportActionCompose extends React.Component {
         const trimmedComment = this.comment.trim();

         // Don't submit empty comments or comments that exceed the character limit
-        if (this.state.isCommentEmpty || trimmedComment.length > CONST.MAX_COMMENT_LENGTH) {
+        if (this.state.isCommentEmpty || this.graphemer.countGraphemes(trimmedComment) > CONST.MAX_COMMENT_LENGTH) {
             return '';
         }

@@ -530,7 +533,7 @@ class ReportActionCompose extends React.Component {
         const isComposeDisabled = this.props.isDrawerOpen && this.props.isSmallScreenWidth;
         const isBlockedFromConcierge = ReportUtils.chatIncludesConcierge(this.props.report) && User.isBlockedFromConcierge(this.props.blockedFromConcierge);
         const inputPlaceholder = this.getInputPlaceholder();
-        const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH;
+        const hasExceededMaxCommentLength = this.graphemer.countGraphemes(this.comment) > CONST.MAX_COMMENT_LENGTH;

         return (
             <View style={[
@@ -723,7 +726,7 @@ class ReportActionCompose extends React.Component {
                 >
                     {!this.props.isSmallScreenWidth && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}
                     <ReportTypingIndicator reportID={this.props.reportID} />
-                    <ExceededCommentLength commentLength={this.comment.length} />
+                    <ExceededCommentLength commentLength={this.graphemer.countGraphemes(this.comment)} />
                 </View>
                 {this.state.isDraggingOver && <ReportDropUI />}
             </View>
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js
index 97fffebebc..afddb1b9b6 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.js
+++ b/src/pages/home/report/ReportActionItemMessageEdit.js
@@ -1,6 +1,7 @@
 import lodashGet from 'lodash/get';
 import React from 'react';
 import {InteractionManager, Keyboard, View} from 'react-native';
+import Graphemer from 'graphemer';
 import PropTypes from 'prop-types';
 import _ from 'underscore';
 import ExpensiMark from 'expensify-common/lib/ExpensiMark';
@@ -71,6 +72,8 @@ class ReportActionItemMessageEdit extends React.Component {
         this.cancelButtonID = 'cancelButton';
         this.emojiButtonID = 'emojiButton';

+        this.graphemer = new Graphemer();
+
         const parser = new ExpensiMark();
         const draftMessage = parser.htmlToMarkdown(this.props.draftMessage);

@@ -154,7 +157,7 @@ class ReportActionItemMessageEdit extends React.Component {
      */
     publishDraft() {
         // Do nothing if draft exceed the character limit
-        if (this.state.draft.length > CONST.MAX_COMMENT_LENGTH) {
+        if (this.graphemer.countGraphemes(this.state.draft) > CONST.MAX_COMMENT_LENGTH) {
             return;
         }

@@ -214,7 +217,7 @@ class ReportActionItemMessageEdit extends React.Component {
     }

     render() {
-        const hasExceededMaxCommentLength = this.state.draft.length > CONST.MAX_COMMENT_LENGTH;
+        const hasExceededMaxCommentLength = this.graphemer.countGraphemes(this.state.draft) > CONST.MAX_COMMENT_LENGTH;
         return (
             <View style={styles.chatItemMessage}>
                 <View
@@ -279,7 +282,7 @@ class ReportActionItemMessageEdit extends React.Component {
                         onPress={this.publishDraft}
                         text={this.props.translate('common.saveChanges')}
                     />
-                    <ExceededCommentLength commentLength={this.state.draft.length} />
+                    <ExceededCommentLength commentLength={this.graphemer.countGraphemes(this.state.draft)} />
                 </View>
             </View>
         );

Details

Refer to https://github.com/Expensify/App/issues/13988#issuecomment-1377111108

Update

I have installed and used graphemer which is a fork of the initially mentioned repo

PS: Do not forget to actually install graphemer after git apply


As for the BE as previously mentioned we can use grapheme_strlen

mollfpr commented 1 year ago

@priyeshshah11 Regarding your proposal here, so did you mean we can't send a text with more than 10000 characters? I just try sending 15000 text characters, and it's working fine.

getusha commented 1 year ago

@mollfpr did you try my proposal?

priyeshshah11 commented 1 year ago

@priyeshshah11 Regarding your proposal here, so did you mean we can't send a text with more than 10000 characters? I just try sending 15000 text characters, and it's working fine.

Ohh ok, but it is not working for me for some reason πŸ€”

https://user-images.githubusercontent.com/38547776/211578237-a0b71a41-459b-471f-b826-d9683ae8ecfc.mov

Edit: No, you're right sending just 15K characters works

priyeshshah11 commented 1 year ago

It would be handy to get some insight into how the BE calculates the length & what's the exact limit for the messages & how it is handled , etc.

mollfpr commented 1 year ago

@getusha Yes, I did. I'm using 😁 where it counts as 2 characters, so 30000 characters with that emoji is 15000 emojis right? But your proposal is to count the emoji as 59999 characters.

Screen Shot 2023-01-10 at 21 38 45

I'm still confused about the expected result, are 15000 emojis should be counted as 15000 characters?

cc @stitesExpensify

getusha commented 1 year ago

@mollfpr every emoji differs which i am counting the unicode length which is ['\uD83D', '\uDE01'] combined. and you can see how it works

priyeshshah11 commented 1 year ago

IMO the expected result should be that the message gets sent successfully when there is no error in the UI (i.e. when characters <= 15000). I don't think we should allow 15000 emojis if the BE only allows 15K characters as that could cause storage issues in the BE.

getusha commented 1 year ago

You can get some insights and understanding here https://towardsdatascience.com/emojis-in-your-data-9a5513ead2dd

the Back end stored as Unicode and counts the length with the unicode

@mollfpr every emoji differs which i am counting the unicode length which is ['\uD83D', '\uDE01'] combined. and you can see how it works

mollfpr commented 1 year ago

@getusha I'm still confused about your proposal. I can't send 15000 and I even can't send the 3750 emojis below.

Screen Shot 2023-01-10 at 21 49 21
mollfpr commented 1 year ago

Expected Result: Emojis should be sent since it's exactly 15000 and no error message should be.

@getusha I just followed the expected result. I didn't understand what your proposal do with the expected result.

getusha commented 1 year ago

@mollfpr

 "πŸ‘©β€πŸ‘©β€πŸ‘¦β€πŸ‘¦".split("") = ['\uD83D', '\uDC69', '‍', '\uD83D', '\uDC69', '‍', '\uD83D', '\uDC66', '‍', '\uD83D', '\uDC66']

Emojis are typically stored in a database in their encoded form, such as Unicode. Unicode is a standardized character encoding that assigns a unique code point to each character and symbol

At the first place the emojis count not 15,000 right? it was 5792, with wrong calculation which was counting only the unicode list length and not the unicode characters. makes sense?

mollfpr commented 1 year ago

@getusha In this comment I'm testing with 15000 emojis (30000 characters where each emoji is 2 characters).

getusha commented 1 year ago

@mollfpr it's not 2 characters it is two set of characters

"😁".split("") = ['\uD83D', '\uDE01']

that's why it is returning 2

getusha commented 1 year ago

the 😁 is equals to "\uD83D\uDE01" and stored in the database

https://github.com/Expensify/App/issues/13988#issuecomment-1376136555

mollfpr commented 1 year ago

@getusha So I'm correct that's in here I try to send 15000 emojis and I should be able to send those emojis with your proposal?

getusha commented 1 year ago

@mollfpr if it counts as less than 15,000 yes

priyeshshah11 commented 1 year ago

the 😁 is equals to "\uD83D\uDE01" and stored in the database

#13988 (comment)

@getusha even if you do "\uD83D\uDE01".length it returns 2 so it would be weird if the BD & DB thinks it is more than 2 characters.

getusha commented 1 year ago

@priyeshshah11 the \ is skipping it can you remove it and try?