From eb667bc436ff6357e35bacd3fb71aa0486663a23 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 24 Aug 2021 15:31:20 +0530 Subject: [PATCH] close #2 : added autocomplete for display name & replace fusejs --- .../organisms/channel/ChannelViewCmdBar.jsx | 281 +++++++++--------- .../organisms/channel/ChannelViewCmdBar.scss | 15 +- .../organisms/channel/ChannelViewInput.jsx | 18 +- src/app/organisms/emoji-board/emoji.js | 8 +- src/client/state/navigation.js | 10 +- src/util/AsyncSearch.js | 2 +- 6 files changed, 183 insertions(+), 151 deletions(-) diff --git a/src/app/organisms/channel/ChannelViewCmdBar.jsx b/src/app/organisms/channel/ChannelViewCmdBar.jsx index 41458ef..35dc0ac 100644 --- a/src/app/organisms/channel/ChannelViewCmdBar.jsx +++ b/src/app/organisms/channel/ChannelViewCmdBar.jsx @@ -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() { {'>@people_name'} Autofill command :emoji_name: + @name )} 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) => ( { fireCmd({ prefix: cmdPrefix, - slug: roomSlug, - result: finding.item, + option, + result: cmd, }); }} > - {finding.item.name} + {`${cmd.name}${cmd.isOptions ? cmdOptString : ''}`} )); } - 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 ( - { - fireCmd({ - prefix: cmdPrefix, - slug: cmdSlug, - option: cmdSlugOption, - result: finding.item, - }); - }} - > - {`${finding.item.name}${option}`} - - ); - }); + function getRoomsSuggestion(cmdPrefix, rooms) { + return rooms.map((room) => ( + { + fireCmd({ + prefix: cmdPrefix, + result: room, + }); + }} + > + {room.name} + + )); } - 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) => ( 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(), }), }, )) } + {`:${emoji.shortcode}:`} + + )); + } + + function getNameSuggestion(namePrefix, members) { + return members.map((member) => ( + { + fireCmd({ + prefix: namePrefix, + result: member, + }); + }} + > + {member.name} )); } - 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 (
@@ -408,8 +423,8 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
{cmd === null && } - {cmd !== null && typeof cmd.slug === 'undefined' &&
} - {cmd !== null && typeof cmd.slug === 'string' && TAB} + {cmd !== null && typeof cmd.suggestions === 'undefined' &&
} + {cmd !== null && typeof cmd.suggestions !== 'undefined' && TAB}
{cmd === null && ( @@ -419,10 +434,10 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) { viewEvent={viewEvent} /> )} - {cmd !== null && typeof cmd.slug === 'undefined' && {getCmdActivationMessage(cmd.prefix)}} - {cmd !== null && typeof cmd.slug === 'string' && ( + {cmd !== null && typeof cmd.suggestions === 'undefined' && {getCmdActivationMessage(cmd.prefix)}} + {cmd !== null && typeof cmd.suggestions !== 'undefined' && ( -
{getCmdSuggestions(cmd, fireCmd, viewEvent)}
+
{getCmdSuggestions(cmd, fireCmd)}
)}
diff --git a/src/app/organisms/channel/ChannelViewCmdBar.scss b/src/app/organisms/channel/ChannelViewCmdBar.scss index 29d3ae9..dc8a981 100644 --- a/src/app/organisms/channel/ChannelViewCmdBar.scss +++ b/src/app/organisms/channel/ChannelViewCmdBar.scss @@ -117,14 +117,10 @@ 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 { @@ -136,4 +132,13 @@ 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 diff --git a/src/app/organisms/channel/ChannelViewInput.jsx b/src/app/organisms/channel/ChannelViewInput.jsx index 957c380..3a771f5 100644 --- a/src/app/organisms/channel/ChannelViewInput.jsx +++ b/src/app/organisms/channel/ChannelViewInput.jsx @@ -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); } diff --git a/src/app/organisms/emoji-board/emoji.js b/src/app/organisms/emoji-board/emoji.js index a57ef98..315b139 100644 --- a/src/app/organisms/emoji-board/emoji.js +++ b/src/app/organisms/emoji-board/emoji.js @@ -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, diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index b482783..6dcf39f 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -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; diff --git a/src/util/AsyncSearch.js b/src/util/AsyncSearch.js index 1751719..f2ac04c 100644 --- a/src/util/AsyncSearch.js +++ b/src/util/AsyncSearch.js @@ -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; } }