Skip to content
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

Render Emoji when typed with emoji code #10165

Merged
merged 36 commits into from
Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
00c4e27
render emoji when typing the emoji code
Karim-30 Aug 1, 2022
fe70b1f
Merge branch 'Expensify:main' into render_emoji
Karim-30 Aug 2, 2022
fbceff8
make a separate TrieNode class file
Karim-30 Aug 2, 2022
736f5ce
Merge branch 'render_emoji' of https://github.com/Karim-30/App into r…
Karim-30 Aug 2, 2022
5f0f994
retrive empty line comments
Karim-30 Aug 3, 2022
d36d981
add unit tests
Karim-30 Aug 3, 2022
994e08d
add unit tests
Karim-30 Aug 3, 2022
46f67e9
remove emoji keyword from Trie and Trie node
Karim-30 Aug 5, 2022
3e1bb7a
Merge branch 'Expensify:main' into render_emoji
Karim-30 Aug 5, 2022
43fa12d
remove emoji keyword from TrieNode file and fix return typo
Karim-30 Aug 5, 2022
08bd89c
remove emoji keyword from TrieNode file and fix return typo
Karim-30 Aug 5, 2022
db64e0e
Add search feature and some improvements to the Trie
Karim-30 Aug 11, 2022
cafe68f
Add search feature and some improvements to the Trie
Karim-30 Aug 11, 2022
b4352b8
Remove Trie and TrieNode files from src/lib folder
Karim-30 Aug 11, 2022
63a185d
Remove a comment line
Karim-30 Aug 16, 2022
fc97ed8
Merge branch 'Expensify:main' into render_emoji
Karim-30 Aug 23, 2022
fbbbd55
fix lint issues and remove unnecessary comment
Karim-30 Aug 25, 2022
98bf16e
return an array of matching words
Karim-30 Aug 30, 2022
b37fcaa
Find only the first 5 matching emojis
Karim-30 Sep 1, 2022
f068b7e
Merge branch 'Expensify:main' into render_emoji
Karim-30 Sep 1, 2022
78cbfae
Use getMetaData method and add comments
Karim-30 Sep 9, 2022
42625de
Add aliases
Karim-30 Sep 18, 2022
ed62ce4
Add getCode method
Karim-30 Sep 22, 2022
62a63d9
Fetch upstream
Karim-30 Sep 22, 2022
64dad95
Revert "Fetch upstream"
Karim-30 Sep 22, 2022
e268e04
Merge branch 'main' into render_emoji
Karim-30 Sep 22, 2022
07dbbfd
Rename containChar param and update method return type
Karim-30 Sep 25, 2022
3439c27
Remove getters&setters, use limit param and add some tests
Karim-30 Sep 26, 2022
74f915e
Use normal function
Karim-30 Sep 26, 2022
c195164
Rename isWord and some params
Karim-30 Sep 26, 2022
eb3bff9
Merge branch 'main' into render_emoji
Karim-30 Oct 6, 2022
f84672a
Rename newComment param to comment
Karim-30 Oct 6, 2022
13ea471
Remove empty lines and update some params
Karim-30 Oct 11, 2022
6f1b47c
Remove unnecessary returns
Karim-30 Oct 11, 2022
af2dff7
rename isLeaf, make trie case insensitive, correct limit value, add u…
Karim-30 Oct 15, 2022
4f790a8
Use early return and remove a comment
Karim-30 Oct 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,363 changes: 1,363 additions & 0 deletions assets/emojis.js

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions src/libs/EmojiTrie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import emojis from '../../assets/emojis';
import Trie from './Trie';

// Create a Trie object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (non blocking): this comment could be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const emojisTrie = new Trie();

// Inserting all emojis into the Trie object
for (let i = 0; i < emojis.length; i++) {
if (emojis[i].name) {
emojisTrie.add(emojis[i].name, emojis[i].code);
}
}

export default emojisTrie;
20 changes: 20 additions & 0 deletions src/libs/EmojiUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import moment from 'moment';
import Str from 'expensify-common/lib/str';
import CONST from '../CONST';
import * as User from './actions/User';
import emojisTrie from './EmojiTrie';

/**
* Get the unicode code of an emoji in base 16.
Expand Down Expand Up @@ -199,10 +200,29 @@ function addToFrequentlyUsedEmojis(frequentlyUsedEmojis, newEmoji) {
User.updateFrequentlyUsedEmojis(frequentEmojiList);
}

/**
* Replace any emoji name in a text with the emoji icon
* @param {String} text
* @returns {String}
*/
function replaceEmojis(text) {
let newText = text;
const emojiNames = text.match(/:[\w+-]+:/g);
if (!emojiNames || emojiNames.length === 0) { return text; }
for (let i = 0; i < emojiNames.length; i++) {
const checkEmoji = emojisTrie.isEmoji(emojiNames[i].slice(1, -1));
if (checkEmoji.found) {
newText = newText.replace(emojiNames[i], checkEmoji.code);
}
}
return newText;
}

export {
isSingleEmoji,
getDynamicHeaderIndices,
mergeEmojisWithFrequentlyUsedEmojis,
addToFrequentlyUsedEmojis,
containsOnlyEmojis,
replaceEmojis,
};
49 changes: 49 additions & 0 deletions src/libs/Trie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import TrieNode from './TrieNode';

/** Class representing a Trie. */
class Trie {
constructor() {
this.root = new TrieNode();
}

/**
* Add an emoji into the Trie
* @param {String} input
* @param {String} code
* @param {TrieNode} node
* @return {Object}
*/
add(input, code, node = this.root) {
if (input.length === 0) {
node.setEnd();
node.setCode(code);
return;
} if (!node.keys.has(input[0])) {
node.keys.set(input[0], new TrieNode());
return this.add(input.substring(1), code, node.keys.get(input[0]));
}
return this.add(input.substring(1), code, node.keys.get(input[0]));
}

/**
* Check if the emoji is exist in the Trie.
* @param {String} emoji
* @return {Object}
*/
isEmoji(emoji) {
let node = this.root;
while (emoji.length > 1) {
if (!node.keys.has(emoji[0])) {
// return false;
return {found: false};
}
node = node.keys.get(emoji[0]);
// eslint-disable-next-line no-param-reassign
emoji = emoji.substring(1);
}
const found = !!((node.keys.has(emoji) && node.keys.get(emoji).isEnd()));
return {found, code: node.keys.get(emoji).getCode()};
}
}

export default Trie;
44 changes: 44 additions & 0 deletions src/libs/TrieNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/** Class representing a Trie node. */
class TrieNode {
/**
* Reset all attributes to default values.
*/
constructor() {
this.keys = new Map();
this.end = false;
this.code = '';
}

/**
* Make the current node an end node.
*/
setEnd() {
this.end = true;
}

/**
* Check if the current node is an end node.
* @returns {Boolean}
*/
isEnd() {
return this.end;
}

/**
* Set the node code, code represent an emoji
* @param {String} code
*/
setCode(code) {
this.code = code;
}

/**
* Get the node code, code represent an emoji
* @return {String}
*/
getCode() {
return this.code;
}
}

export default TrieNode;
20 changes: 13 additions & 7 deletions src/pages/home/report/ReportActionCompose.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import toggleReportActionComposeView from '../../../libs/toggleReportActionCompo
import OfflineIndicator from '../../../components/OfflineIndicator';
import ExceededCommentLength from '../../../components/ExceededCommentLength';
import withNavigationFocus from '../../../components/withNavigationFocus';
import * as EmojiUtils from '../../../libs/EmojiUtils';

const propTypes = {
/** Beta features list */
Expand Down Expand Up @@ -372,7 +373,8 @@ class ReportActionCompose extends React.Component {
* @param {String} newComment
*/
updateComment(newComment) {
this.textInput.setNativeProps({text: newComment});
const textWithEmojis = EmojiUtils.replaceEmojis(newComment);
this.textInput.setNativeProps({text: textWithEmojis});
this.setState({
isCommentEmpty: !!newComment.match(/^(\s|`)*$/),
});
Expand All @@ -387,9 +389,9 @@ class ReportActionCompose extends React.Component {
Report.setReportWithDraft(this.props.reportID.toString(), false);
}

this.comment = newComment;
this.debouncedSaveReportComment(newComment);
if (newComment) {
this.comment = textWithEmojis;
this.debouncedSaveReportComment(textWithEmojis);
if (textWithEmojis) {
this.debouncedBroadcastUserIsTyping();
}
}
Expand Down Expand Up @@ -496,7 +498,11 @@ class ReportActionCompose extends React.Component {
const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH;

return (
<View style={[shouldShowReportRecipientLocalTime && styles.chatItemComposeWithFirstRow, this.props.isComposerFullSize && styles.chatItemFullComposeRow]}>
<View style={[
shouldShowReportRecipientLocalTime && !this.props.network.isOffline && styles.chatItemComposeWithFirstRow,
this.props.isComposerFullSize && styles.chatItemFullComposeRow,
]}
>
{shouldShowReportRecipientLocalTime
&& <ParticipantLocalTime participant={reportRecipient} />}
<View style={[
Expand Down Expand Up @@ -673,8 +679,8 @@ class ReportActionCompose extends React.Component {
</Tooltip>
</View>
</View>
<View style={[styles.chatItemComposeSecondaryRow, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}>
<OfflineIndicator />
<View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}>
{!this.props.isSmallScreenWidth && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}
<ReportTypingIndicator reportID={this.props.reportID} />
<ExceededCommentLength commentLength={this.comment.length} />
</View>
Expand Down
14 changes: 8 additions & 6 deletions src/pages/home/report/ReportActionItemMessageEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import compose from '../../../libs/compose';
import EmojiPickerButton from '../../../components/EmojiPicker/EmojiPickerButton';
import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
import VirtualKeyboard from '../../../libs/VirtualKeyboard';
import * as EmojiUtils from '../../../libs/EmojiUtils';

const propTypes = {
/** All the data of the action */
Expand Down Expand Up @@ -94,16 +95,17 @@ class ReportActionItemMessageEdit extends React.Component {
/**
* Update the value of the draft in Onyx
*
* @param {String} newDraft
* @param {String} draft
*/
updateDraft(newDraft) {
this.textInput.setNativeProps({text: newDraft});
this.setState({draft: newDraft});
updateDraft(draft) {
const textWithEmojis = EmojiUtils.replaceEmojis(draft);
this.textInput.setNativeProps({text: textWithEmojis});
this.setState({draft: textWithEmojis});

// This component is rendered only when draft is set to a non-empty string. In order to prevent component
// unmount when user deletes content of textarea, we set previous message instead of empty string.
if (newDraft.trim().length > 0) {
this.debouncedSaveDraft(newDraft);
if (textWithEmojis.trim().length > 0) {
this.debouncedSaveDraft(textWithEmojis);
} else {
this.debouncedSaveDraft(this.props.action.message[0].html);
}
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/EmojiCodeTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as EmojiUtils from '../../src/libs/EmojiUtils';

describe('EmojiCode', () => {
it('Test replacing emoji codes with emojis inside a text', () => {
const text = 'Hi :smile:';
expect(EmojiUtils.replaceEmojis(text)).toBe('Hi 😄');
});
});
13 changes: 13 additions & 0 deletions tests/unit/EmojiTrieTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Trie from '../../src/libs/Trie';

describe('Trie', () => {
it('Test if a Trie node is exist in the Trie', () => {
const emojisTrie = new Trie();
emojisTrie.add('grinning', '😀');
emojisTrie.add('grin', '😁');
emojisTrie.add('joy', '😂');
emojisTrie.add('rofl', '🤣');
expect(emojisTrie.isEmoji('grinning')).toEqual({found: true, code: '😀'});
expect(emojisTrie.isEmoji('eyes')).toEqual({found: false});
});
});