Add recently used section to emoji board (#373)
authorginnyTheCat <ginnythecat@lelux.net>
Thu, 17 Mar 2022 11:49:14 +0000 (12:49 +0100)
committerGitHub <noreply@github.com>
Thu, 17 Mar 2022 11:49:14 +0000 (17:19 +0530)
* Add recent section to emoji board

* Add section to emoji board sidebar

* Add emoji limit like element web has

* Ignore custom emojis

* Filter out invalid emojis

* Update heart icon with clock

Co-authored-by: Krishan <33421343+kfiven@users.noreply.github.com>
src/app/organisms/emoji-board/EmojiBoard.jsx
src/app/organisms/emoji-board/recent.js [new file with mode: 0644]
src/app/organisms/room/RoomViewCmdBar.jsx

index 1e61abd811b63b4004358befede080a907200d97..864a0bf60140f7a343d9e1bf28dd151963e046ec 100644 (file)
@@ -12,6 +12,7 @@ import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import navigation from '../../../client/state/navigation';
 import AsyncSearch from '../../../util/AsyncSearch';
+import { addRecentEmoji, getRecentEmojis } from './recent';
 
 import Text from '../../atoms/text/Text';
 import RawIcon from '../../atoms/system-icons/RawIcon';
@@ -20,6 +21,7 @@ import Input from '../../atoms/input/Input';
 import ScrollView from '../../atoms/scroll/ScrollView';
 
 import SearchIC from '../../../../public/res/ic/outlined/search.svg';
+import RecentClockIC from '../../../../public/res/ic/outlined/recent-clock.svg';
 import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
 import DogIC from '../../../../public/res/ic/outlined/dog.svg';
 import CupIC from '../../../../public/res/ic/outlined/cup.svg';
@@ -29,10 +31,11 @@ import BulbIC from '../../../../public/res/ic/outlined/bulb.svg';
 import PeaceIC from '../../../../public/res/ic/outlined/peace.svg';
 import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
 
+const ROW_EMOJIS_COUNT = 7;
+
 const EmojiGroup = React.memo(({ name, groupEmojis }) => {
   function getEmojiBoard() {
     const emojiBoard = [];
-    const ROW_EMOJIS_COUNT = 7;
     const totalEmojis = groupEmojis.length;
 
     for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
@@ -147,8 +150,9 @@ function EmojiBoard({ onSelect, searchRef }) {
   function selectEmoji(e) {
     if (isTargetNotEmoji(e.target)) return;
 
-    const emoji = e.target;
-    onSelect(getEmojiDataFromTarget(emoji));
+    const emoji = getEmojiDataFromTarget(e.target);
+    onSelect(emoji);
+    if (emoji.hexcode) addRecentEmoji(emoji.unicode);
   }
 
   function setEmojiInfo(emoji) {
@@ -188,6 +192,9 @@ function EmojiBoard({ onSelect, searchRef }) {
   }
 
   const [availableEmojis, setAvailableEmojis] = useState([]);
+  const [recentEmojis, setRecentEmojis] = useState([]);
+
+  const recentOffset = recentEmojis.length > 0 ? 1 : 0;
 
   useEffect(() => {
     const updateAvailableEmoji = (selectedRoomId) => {
@@ -215,6 +222,9 @@ function EmojiBoard({ onSelect, searchRef }) {
     const onOpen = () => {
       searchRef.current.value = '';
       handleSearchChange();
+
+      // only update when board is getting opened to prevent shifting UI
+      setRecentEmojis(getRecentEmojis(3 * ROW_EMOJIS_COUNT));
     };
 
     navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
@@ -230,7 +240,7 @@ function EmojiBoard({ onSelect, searchRef }) {
     const $emojiContent = scrollEmojisRef.current.firstElementChild;
     const groupCount = $emojiContent.childElementCount;
     if (groupCount > emojiGroups.length) {
-      tabIndex += groupCount - emojiGroups.length - availableEmojis.length;
+      tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset;
     }
     $emojiContent.children[tabIndex].scrollIntoView();
   }
@@ -246,6 +256,7 @@ function EmojiBoard({ onSelect, searchRef }) {
           <ScrollView ref={scrollEmojisRef} autoHide>
             <div onMouseMove={hoverEmoji} onClick={selectEmoji}>
               <SearchedEmoji />
+              {recentEmojis.length > 0 && <EmojiGroup name="Recently used" groupEmojis={recentEmojis} />}
               {
                 availableEmojis.map((pack) => (
                   <EmojiGroup
@@ -271,13 +282,21 @@ function EmojiBoard({ onSelect, searchRef }) {
       </div>
       <ScrollView invisible>
         <div className="emoji-board__nav">
+          {recentEmojis.length > 0 && (
+            <IconButton
+              onClick={() => openGroup(0)}
+              src={RecentClockIC}
+              tooltip="Recent"
+              tooltipPlacement="right"
+            />
+          )}
           <div className="emoji-board__nav-custom">
             {
               availableEmojis.map((pack) => {
                 const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc);
                 return (
                   <IconButton
-                    onClick={() => openGroup(pack.packIndex)}
+                    onClick={() => openGroup(recentOffset + pack.packIndex)}
                     src={src}
                     key={pack.packIndex}
                     tooltip={pack.displayName}
@@ -301,7 +320,7 @@ function EmojiBoard({ onSelect, searchRef }) {
                 [7, FlagIC, 'Flags'],
               ].map(([indx, ico, name]) => (
                 <IconButton
-                  onClick={() => openGroup(availableEmojis.length + indx)}
+                  onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
                   key={indx}
                   src={ico}
                   tooltip={name}
diff --git a/src/app/organisms/emoji-board/recent.js b/src/app/organisms/emoji-board/recent.js
new file mode 100644 (file)
index 0000000..d175f26
--- /dev/null
@@ -0,0 +1,36 @@
+import initMatrix from '../../../client/initMatrix';
+import { emojis } from './emoji';
+
+const eventType = 'io.element.recent_emoji';
+
+function getRecentEmojisRaw() {
+  return initMatrix.matrixClient.getAccountData(eventType).getContent().recent_emoji ?? [];
+}
+
+export function getRecentEmojis(limit) {
+  const res = [];
+  getRecentEmojisRaw()
+    .sort((a, b) => b[1] - a[1])
+    .find(([unicode]) => {
+      const emoji = emojis.find((e) => e.unicode === unicode);
+      if (emoji) return res.push(emoji) >= limit;
+      return false;
+    });
+  return res;
+}
+
+export function addRecentEmoji(unicode) {
+  const recent = getRecentEmojisRaw();
+  const i = recent.findIndex(([u]) => u === unicode);
+  let entry;
+  if (i < 0) {
+    entry = [unicode, 1];
+  } else {
+    [entry] = recent.splice(i, 1);
+    entry[1] += 1;
+  }
+  recent.unshift(entry);
+  initMatrix.matrixClient.setAccountData(eventType, {
+    recent_emoji: recent.slice(0, 100),
+  });
+}
index 0c723196b38353420844264f6b8188e7868369ed..4ccb20a41a20c3dc2debae9660b5c470ae3f1d9e 100644 (file)
@@ -21,6 +21,7 @@ import AsyncSearch from '../../../util/AsyncSearch';
 import Text from '../../atoms/text/Text';
 import ScrollView from '../../atoms/scroll/ScrollView';
 import FollowingMembers from '../../molecules/following-members/FollowingMembers';
+import { addRecentEmoji } from '../emoji-board/recent';
 
 const commands = [{
   name: 'markdown',
@@ -237,6 +238,7 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
       viewEvent.emit('cmd_fired');
     }
     if (myCmd.prefix === ':') {
+      if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
       viewEvent.emit('cmd_fired', {
         replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
       });