close #2 : added autocomplete for display name & replace fusejs
authorunknown <ajbura@gmail.com>
Tue, 24 Aug 2021 10:01:20 +0000 (15:31 +0530)
committerunknown <ajbura@gmail.com>
Tue, 24 Aug 2021 10:01:20 +0000 (15:31 +0530)
src/app/organisms/channel/ChannelViewCmdBar.jsx
src/app/organisms/channel/ChannelViewCmdBar.scss
src/app/organisms/channel/ChannelViewInput.jsx
src/app/organisms/emoji-board/emoji.js
src/client/state/navigation.js
src/util/AsyncSearch.js

index 41458ef28be5b8f147e0f2cf553271422f064b29..35dc0acee53054da6ac76aa8c48d5ae706a941b8 100644 (file)
@@ -2,7 +2,6 @@
 import React, { useState, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import './ChannelViewCmdBar.scss';
-import Fuse from 'fuse.js';
 import parse from 'html-react-parser';
 import twemoji from 'twemoji';
 
@@ -17,7 +16,8 @@ import {
   openInviteUser,
   openReadReceipts,
 } from '../../../client/action/navigation';
-import { searchEmoji } from '../emoji-board/emoji';
+import { emojis } from '../emoji-board/emoji';
+import AsyncSearch from '../../../util/AsyncSearch';
 
 import Text from '../../atoms/text/Text';
 import Button from '../../atoms/button/Button';
@@ -74,6 +74,7 @@ function CmdHelp() {
           <Text variant="b2">{'>@people_name'}</Text>
           <MenuHeader>Autofill command</MenuHeader>
           <Text variant="b2">:emoji_name:</Text>
+          <Text variant="b2">@name</Text>
         </>
       )}
       render={(toggleMenu) => (
@@ -176,6 +177,7 @@ function getCmdActivationMessage(prefix) {
     '>#': () => genMessage('Go-to command mode activated. ', 'Type channel name for suggestions.'),
     '>@': () => genMessage('Go-to command mode activated. ', 'Type people name for suggestions.'),
     ':': () => genMessage('Emoji autofill command mode activated. ', 'Type emoji shortcut for suggestions.'),
+    '@': () => genMessage('Name autofill command mode activated. ', 'Type name for suggestions.'),
   };
   return cmd[prefix]?.();
 }
@@ -192,163 +194,166 @@ CmdItem.propTypes = {
   children: PropTypes.node.isRequired,
 };
 
-function searchInRoomIds(roomIds, term) {
-  const rooms = roomIds.map((rId) => {
-    const room = initMatrix.matrixClient.getRoom(rId);
-    return {
-      name: room.name,
-      roomId: room.roomId,
-    };
-  });
-  const fuse = new Fuse(rooms, {
-    includeScore: true,
-    keys: ['name'],
-    threshold: '0.3',
-  });
-  return fuse.search(term);
-}
-
-function searchCommands(term) {
-  const fuse = new Fuse(commands, {
-    includeScore: true,
-    keys: ['name'],
-    threshold: '0.3',
-  });
-  return fuse.search(term);
-}
-
-let perfectMatchCmd = null;
-function getCmdSuggestions({ prefix, slug }, fireCmd, viewEvent) {
-  function getRoomsSuggestion(cmdPrefix, rooms, roomSlug) {
-    const result = searchInRoomIds(rooms, roomSlug);
-    if (result.length === 0) viewEvent.emit('cmd_error');
-    perfectMatchCmd = {
-      prefix: cmdPrefix,
-      slug: roomSlug,
-      result: result[0]?.item || null,
-    };
-    return result.map((finding) => (
+function getCmdSuggestions({ prefix, option, suggestions }, fireCmd) {
+  function getGenCmdSuggestions(cmdPrefix, cmds) {
+    const cmdOptString = (typeof option === 'string') ? `/${option}` : '/?';
+    return cmds.map((cmd) => (
       <CmdItem
-        key={finding.item.roomId}
+        key={cmd.name}
         onClick={() => {
           fireCmd({
             prefix: cmdPrefix,
-            slug: roomSlug,
-            result: finding.item,
+            option,
+            result: cmd,
           });
         }}
       >
-        <Text variant="b2">{finding.item.name}</Text>
+        <Text variant="b2">{`${cmd.name}${cmd.isOptions ? cmdOptString : ''}`}</Text>
       </CmdItem>
     ));
   }
 
-  function getGenCmdSuggestions(cmdPrefix, cmdSlug) {
-    const cmdSlugParts = cmdSlug.split('/');
-    const cmdSlugOption = cmdSlugParts[1];
-    const result = searchCommands(cmdSlugParts[0]);
-    if (result.length === 0) viewEvent.emit('cmd_error');
-    perfectMatchCmd = {
-      prefix: cmdPrefix,
-      slug: cmdSlug,
-      option: cmdSlugOption,
-      result: result[0]?.item || null,
-    };
-    return result.map((finding) => {
-      let option = '';
-      if (finding.item.isOptions) {
-        if (typeof cmdSlugOption === 'string') option = `/${cmdSlugOption}`;
-        else option = '/?';
-      }
-      return (
-        <CmdItem
-          key={finding.item.name}
-          onClick={() => {
-            fireCmd({
-              prefix: cmdPrefix,
-              slug: cmdSlug,
-              option: cmdSlugOption,
-              result: finding.item,
-            });
-          }}
-        >
-          <Text variant="b2">{`${finding.item.name}${option}`}</Text>
-        </CmdItem>
-      );
-    });
+  function getRoomsSuggestion(cmdPrefix, rooms) {
+    return rooms.map((room) => (
+      <CmdItem
+        key={room.roomId}
+        onClick={() => {
+          fireCmd({
+            prefix: cmdPrefix,
+            result: room,
+          });
+        }}
+      >
+        <Text variant="b2">{room.name}</Text>
+      </CmdItem>
+    ));
   }
 
-  function getEmojiSuggestion(emPrefix, shortcutSlug) {
-    let searchTerm = shortcutSlug;
-    if (searchTerm.length <= 3) {
-      if (searchTerm.match(/^[-]?(\))/)) searchTerm = 'smile';
-      else if (searchTerm.match(/^[-]?(s|S)/)) searchTerm = 'confused';
-      else if (searchTerm.match(/^[-]?(o|O|0)/)) searchTerm = 'astonished';
-      else if (searchTerm.match(/^[-]?(\|)/)) searchTerm = 'neutral_face';
-      else if (searchTerm.match(/^[-]?(d|D)/)) searchTerm = 'grin';
-      else if (searchTerm.match(/^[-]?(\/)/)) searchTerm = 'frown';
-      else if (searchTerm.match(/^[-]?(p|P)/)) searchTerm = 'stick_out_tongue';
-      else if (searchTerm.match(/^'[-]?(\()/)) searchTerm = 'cry';
-      else if (searchTerm.match(/^[-]?(x|X)/)) searchTerm = 'dizzy_face';
-      else if (searchTerm.match(/^[-]?(\()/)) searchTerm = 'pleading_face';
-      else if (searchTerm.match(/^[-]?(\$)/)) searchTerm = 'money';
-      else if (searchTerm.match(/^(<3)/)) searchTerm = 'heart';
-    }
-    const result = searchEmoji(searchTerm);
-    if (result.length === 0) viewEvent.emit('cmd_error');
-    perfectMatchCmd = {
-      prefix: emPrefix,
-      slug: shortcutSlug,
-      result: result[0]?.item || null,
-    };
-    return result.map((finding) => (
+  function getEmojiSuggestion(emPrefix, emos) {
+    return emos.map((emoji) => (
       <CmdItem
-        key={finding.item.hexcode}
+        key={emoji.hexcode}
         onClick={() => fireCmd({
           prefix: emPrefix,
-          slug: shortcutSlug,
-          result: finding.item,
+          result: emoji,
         })}
       >
         {
           parse(twemoji.parse(
-            finding.item.unicode,
+            emoji.unicode,
             {
               attributes: () => ({
-                unicode: finding.item.unicode,
-                shortcodes: finding.item.shortcodes?.toString(),
+                unicode: emoji.unicode,
+                shortcodes: emoji.shortcodes?.toString(),
               }),
             },
           ))
         }
+        <Text variant="b2">{`:${emoji.shortcode}:`}</Text>
+      </CmdItem>
+    ));
+  }
+
+  function getNameSuggestion(namePrefix, members) {
+    return members.map((member) => (
+      <CmdItem
+        key={member.userId}
+        onClick={() => {
+          fireCmd({
+            prefix: namePrefix,
+            result: member,
+          });
+        }}
+      >
+        <Text variant="b2">{member.name}</Text>
       </CmdItem>
     ));
   }
 
-  const { roomList } = initMatrix;
   const cmd = {
-    '/': (command) => getGenCmdSuggestions(prefix, command),
-    '>*': (space) => getRoomsSuggestion(prefix, [...roomList.spaces], space),
-    '>#': (channel) => getRoomsSuggestion(prefix, [...roomList.rooms], channel),
-    '>@': (people) => getRoomsSuggestion(prefix, [...roomList.directs], people),
-    ':': (emojiShortcut) => getEmojiSuggestion(prefix, emojiShortcut),
+    '/': (cmds) => getGenCmdSuggestions(prefix, cmds),
+    '>*': (spaces) => getRoomsSuggestion(prefix, spaces),
+    '>#': (channels) => getRoomsSuggestion(prefix, channels),
+    '>@': (peoples) => getRoomsSuggestion(prefix, peoples),
+    ':': (emos) => getEmojiSuggestion(prefix, emos),
+    '@': (members) => getNameSuggestion(prefix, members),
   };
-  return cmd[prefix]?.(slug);
+  return cmd[prefix]?.(suggestions);
 }
 
+const asyncSearch = new AsyncSearch();
+let cmdPrefix;
+let cmdOption;
 function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
   const [cmd, setCmd] = useState(null);
 
+  function displaySuggestions(suggestions) {
+    if (suggestions.length === 0) {
+      setCmd({ prefix: cmd?.prefix || cmdPrefix, error: 'No suggestion found.' });
+      viewEvent.emit('cmd_error');
+      return;
+    }
+    setCmd({ prefix: cmd?.prefix || cmdPrefix, suggestions, option: cmdOption });
+  }
+
   function processCmd(prefix, slug) {
-    setCmd({ prefix, slug });
+    let searchTerm = slug;
+    cmdOption = undefined;
+    cmdPrefix = prefix;
+    if (prefix === '/') {
+      const cmdSlugParts = slug.split('/');
+      [searchTerm, cmdOption] = cmdSlugParts;
+    }
+    if (prefix === ':') {
+      if (searchTerm.length <= 3) {
+        if (searchTerm.match(/^[-]?(\))$/)) searchTerm = 'smile';
+        else if (searchTerm.match(/^[-]?(s|S)$/)) searchTerm = 'confused';
+        else if (searchTerm.match(/^[-]?(o|O|0)$/)) searchTerm = 'astonished';
+        else if (searchTerm.match(/^[-]?(\|)$/)) searchTerm = 'neutral_face';
+        else if (searchTerm.match(/^[-]?(d|D)$/)) searchTerm = 'grin';
+        else if (searchTerm.match(/^[-]?(\/)$/)) searchTerm = 'frown';
+        else if (searchTerm.match(/^[-]?(p|P)$/)) searchTerm = 'stick_out_tongue';
+        else if (searchTerm.match(/^'[-]?(\()$/)) searchTerm = 'cry';
+        else if (searchTerm.match(/^[-]?(x|X)$/)) searchTerm = 'dizzy_face';
+        else if (searchTerm.match(/^[-]?(\()$/)) searchTerm = 'pleading_face';
+        else if (searchTerm.match(/^[-]?(\$)$/)) searchTerm = 'money';
+        else if (searchTerm.match(/^(<3)$/)) searchTerm = 'heart';
+      }
+    }
+
+    asyncSearch.search(searchTerm);
   }
   function activateCmd(prefix) {
     setCmd({ prefix });
-    perfectMatchCmd = null;
+    cmdPrefix = prefix;
+
+    const { roomList, matrixClient } = initMatrix;
+    function getRooms(roomIds) {
+      return roomIds.map((rId) => {
+        const room = matrixClient.getRoom(rId);
+        return {
+          name: room.name,
+          roomId: room.roomId,
+        };
+      });
+    }
+    const setupSearch = {
+      '/': () => asyncSearch.setup(commands, { keys: ['name'], isContain: true }),
+      '>*': () => asyncSearch.setup(getRooms([...roomList.spaces]), { keys: ['name'], limit: 20 }),
+      '>#': () => asyncSearch.setup(getRooms([...roomList.rooms]), { keys: ['name'], limit: 20 }),
+      '>@': () => asyncSearch.setup(getRooms([...roomList.directs]), { keys: ['name'], limit: 20 }),
+      ':': () => asyncSearch.setup(emojis, { keys: ['shortcode'], limit: 20 }),
+      '@': () => asyncSearch.setup(matrixClient.getRoom(roomId).getJoinedMembers().map((member) => ({
+        name: member.name,
+        userId: member.userId.slice(1),
+      })), { keys: ['name', 'userId'], limit: 20 }),
+    };
+    setupSearch[prefix]?.();
   }
   function deactivateCmd() {
     setCmd(null);
-    perfectMatchCmd = null;
+    cmdOption = undefined;
+    cmdPrefix = undefined;
   }
   function fireCmd(myCmd) {
     if (myCmd.prefix.match(/^>[*#@]$/)) {
@@ -364,34 +369,44 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
         replace: myCmd.result.unicode,
       });
     }
+    if (myCmd.prefix === '@') {
+      viewEvent.emit('cmd_fired', {
+        replace: myCmd.result.name,
+      });
+    }
     deactivateCmd();
   }
   function executeCmd() {
-    if (perfectMatchCmd === null) return;
-    if (perfectMatchCmd.result === null) return;
-    fireCmd(perfectMatchCmd);
-  }
-  function errorCmd() {
-    setCmd({ error: 'No suggestion found.' });
+    if (cmd.suggestions.length === 0) return;
+    fireCmd({
+      prefix: cmd.prefix,
+      option: cmd.option,
+      result: cmd.suggestions[0],
+    });
   }
 
   useEffect(() => {
     viewEvent.on('cmd_activate', activateCmd);
-    viewEvent.on('cmd_process', processCmd);
     viewEvent.on('cmd_deactivate', deactivateCmd);
-    viewEvent.on('cmd_exe', executeCmd);
-    viewEvent.on('cmd_error', errorCmd);
     return () => {
       deactivateCmd();
       viewEvent.removeListener('cmd_activate', activateCmd);
-      viewEvent.removeListener('cmd_process', processCmd);
       viewEvent.removeListener('cmd_deactivate', deactivateCmd);
-      viewEvent.removeListener('cmd_exe', executeCmd);
-      viewEvent.removeListener('cmd_error', errorCmd);
     };
   }, [roomId]);
 
-  if (cmd !== null && typeof cmd.error !== 'undefined') {
+  useEffect(() => {
+    viewEvent.on('cmd_process', processCmd);
+    viewEvent.on('cmd_exe', executeCmd);
+    asyncSearch.on(asyncSearch.RESULT_SENT, displaySuggestions);
+    return () => {
+      viewEvent.removeListener('cmd_process', processCmd);
+      viewEvent.removeListener('cmd_exe', executeCmd);
+      asyncSearch.removeListener(asyncSearch.RESULT_SENT, displaySuggestions);
+    };
+  }, [cmd]);
+
+  if (typeof cmd?.error === 'string') {
     return (
       <div className="cmd-bar">
         <div className="cmd-bar__info">
@@ -408,8 +423,8 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
     <div className="cmd-bar">
       <div className="cmd-bar__info">
         {cmd === null && <CmdHelp />}
-        {cmd !== null && typeof cmd.slug === 'undefined' && <div className="cmd-bar__info-indicator" /> }
-        {cmd !== null && typeof cmd.slug === 'string' && <Text variant="b3">TAB</Text>}
+        {cmd !== null && typeof cmd.suggestions === 'undefined' && <div className="cmd-bar__info-indicator" /> }
+        {cmd !== null && typeof cmd.suggestions !== 'undefined' && <Text variant="b3">TAB</Text>}
       </div>
       <div className="cmd-bar__content">
         {cmd === null && (
@@ -419,10 +434,10 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
             viewEvent={viewEvent}
           />
         )}
-        {cmd !== null && typeof cmd.slug === 'undefined' && <Text className="cmd-bar__content-help" variant="b2">{getCmdActivationMessage(cmd.prefix)}</Text>}
-        {cmd !== null && typeof cmd.slug === 'string' && (
+        {cmd !== null && typeof cmd.suggestions === 'undefined' && <Text className="cmd-bar__content-help" variant="b2">{getCmdActivationMessage(cmd.prefix)}</Text>}
+        {cmd !== null && typeof cmd.suggestions !== 'undefined' && (
           <ScrollView horizontal vertical={false} invisible>
-            <div className="cmd-bar__content__suggestions">{getCmdSuggestions(cmd, fireCmd, viewEvent)}</div>
+            <div className="cmd-bar__content__suggestions">{getCmdSuggestions(cmd, fireCmd)}</div>
           </ScrollView>
         )}
       </div>
index 29d3ae9ec352627fc74e0152a1478394f4ead240..dc8a98122c063d4246e7147c68aa19ad14979076 100644 (file)
   border-radius: var(--bo-radius) var(--bo-radius) 0 0;
   cursor: pointer;
 
-  [dir=rtl] & {
-    margin-right: 0;
-    margin-left: var(--sp-extra-tight);
-  }
-
   & .emoji {
     width: 20px;
     height: 20px;
+    margin-right: var(--sp-ultra-tight);
   }
 
   &:hover {
     border-bottom: 2px solid transparent;
     outline: none;
   }
+  
+  [dir=rtl] & {
+    margin-right: 0;
+    margin-left: var(--sp-extra-tight);
+    & .emoji {
+      margin-right: 0;
+      margin-left: var(--sp-ultra-tight);
+    }
+  }
 }
\ No newline at end of file
index 957c3802552218635bddf869ed106822e7989c2c..3a771f51b17e52f3b836d9eeb1fc0878600c76d6 100644 (file)
@@ -29,7 +29,7 @@ import MarkdownIC from '../../../../public/res/ic/outlined/markdown.svg';
 import FileIC from '../../../../public/res/ic/outlined/file.svg';
 import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 
-const CMD_REGEX = /(\/|>[#*@]|:)(\S*)$/;
+const CMD_REGEX = /(\/|>[#*@]|:|@)(\S*)$/;
 let isTyping = false;
 let isCmdActivated = false;
 let cmdCursorPos = null;
@@ -90,20 +90,26 @@ function ChannelViewInput({
 
   function activateCmd(prefix) {
     isCmdActivated = true;
-    inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-positive)';
+    requestAnimationFrame(() => {
+      inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-positive)';
+    });
     rightOptionsA11Y(false);
     viewEvent.emit('cmd_activate', prefix);
   }
   function deactivateCmd() {
     if (inputBaseRef.current !== null) {
-      inputBaseRef.current.style.boxShadow = 'var(--bs-surface-border)';
+      requestAnimationFrame(() => {
+        inputBaseRef.current.style.boxShadow = 'var(--bs-surface-border)';
+      });
       rightOptionsA11Y(true);
     }
     isCmdActivated = false;
     cmdCursorPos = null;
   }
   function errorCmd() {
-    inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-danger)';
+    requestAnimationFrame(() => {
+      inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-danger)';
+    });
   }
   function setCursorPosition(pos) {
     setTimeout(() => {
@@ -242,7 +248,9 @@ function ChannelViewInput({
       return;
     }
     if (!isCmdActivated) activateCmd(cmdPrefix);
-    inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-caution)';
+    requestAnimationFrame(() => {
+      inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-caution)';
+    });
     viewEvent.emit('cmd_process', cmdPrefix, cmdSlug);
   }
 
index a57ef980796338b109a498da35f70b33adf0780f..315b13956d1697e3341995bcaafa6516efac5bcf 100644 (file)
@@ -53,11 +53,15 @@ function addToGroup(emoji) {
 
 const emojis = [];
 emojisData.forEach((emoji) => {
-  const em = { ...emoji, shortcodes: shortcodes[emoji.hexcode] };
+  const myShortCodes = shortcodes[emoji.hexcode];
+  const em = {
+    ...emoji,
+    shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes,
+    shortcodes: myShortCodes,
+  };
   addToGroup(em);
   emojis.push(em);
 });
-
 function searchEmoji(term) {
   const options = {
     includeScore: true,
index b482783672bd02cda490e873cec98f1728917028..6dcf39fd4cac4f570097ecad301737e4f01985ff 100644 (file)
@@ -7,7 +7,7 @@ class Navigation extends EventEmitter {
     super();
 
     this.activeTab = 'channels';
-    this.selectedRoom = null;
+    this.activeRoomId = null;
     this.isPeopleDrawerVisible = true;
   }
 
@@ -15,8 +15,8 @@ class Navigation extends EventEmitter {
     return this.activeTab;
   }
 
-  getActiveRoom() {
-    return this.selectedRoom;
+  getActiveRoomId() {
+    return this.activeRoomId;
   }
 
   navigate(action) {
@@ -26,8 +26,8 @@ class Navigation extends EventEmitter {
         this.emit(cons.events.navigation.TAB_CHANGED, this.activeTab);
       },
       [cons.actions.navigation.SELECT_ROOM]: () => {
-        this.selectedRoom = action.roomId;
-        this.emit(cons.events.navigation.ROOM_SELECTED, this.selectedRoom);
+        this.activeRoomId = action.roomId;
+        this.emit(cons.events.navigation.ROOM_SELECTED, this.activeRoomId);
       },
       [cons.actions.navigation.TOGGLE_PEOPLE_DRAWER]: () => {
         this.isPeopleDrawerVisible = !this.isPeopleDrawerVisible;
index 1751719200d3c3d450db6abcacbbbd45380dd484..f2ac04c14ad7c5df7b4aeae5d3ae9641e269486f 100644 (file)
@@ -82,7 +82,7 @@ class AsyncSearch extends EventEmitter {
         if (lastFindingCount !== thisFindingCount) this._sendFindings();
 
         this.searchUptoIndex = searchIndex + 1;
-        queueMicrotask(() => this._find(thisSessionTimestamp, thisFindingCount));
+        setTimeout(() => this._find(thisSessionTimestamp, thisFindingCount));
         return;
       }
     }