close #2 : added autocomplete for display name & replace fusejs

This commit is contained in:
unknown 2021-08-24 15:31:20 +05:30
parent c81628a66e
commit eb667bc436
6 changed files with 183 additions and 151 deletions

View file

@ -2,7 +2,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './ChannelViewCmdBar.scss'; import './ChannelViewCmdBar.scss';
import Fuse from 'fuse.js';
import parse from 'html-react-parser'; import parse from 'html-react-parser';
import twemoji from 'twemoji'; import twemoji from 'twemoji';
@ -17,7 +16,8 @@ import {
openInviteUser, openInviteUser,
openReadReceipts, openReadReceipts,
} from '../../../client/action/navigation'; } 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 Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
@ -74,6 +74,7 @@ function CmdHelp() {
<Text variant="b2">{'>@people_name'}</Text> <Text variant="b2">{'>@people_name'}</Text>
<MenuHeader>Autofill command</MenuHeader> <MenuHeader>Autofill command</MenuHeader>
<Text variant="b2">:emoji_name:</Text> <Text variant="b2">:emoji_name:</Text>
<Text variant="b2">@name</Text>
</> </>
)} )}
render={(toggleMenu) => ( 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 channel name for suggestions.'),
'>@': () => genMessage('Go-to command mode activated. ', 'Type people 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('Emoji autofill command mode activated. ', 'Type emoji shortcut for suggestions.'),
'@': () => genMessage('Name autofill command mode activated. ', 'Type name for suggestions.'),
}; };
return cmd[prefix]?.(); return cmd[prefix]?.();
} }
@ -192,163 +194,166 @@ CmdItem.propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
}; };
function searchInRoomIds(roomIds, term) { function getCmdSuggestions({ prefix, option, suggestions }, fireCmd) {
const rooms = roomIds.map((rId) => { function getGenCmdSuggestions(cmdPrefix, cmds) {
const room = initMatrix.matrixClient.getRoom(rId); const cmdOptString = (typeof option === 'string') ? `/${option}` : '/?';
return cmds.map((cmd) => (
<CmdItem
key={cmd.name}
onClick={() => {
fireCmd({
prefix: cmdPrefix,
option,
result: cmd,
});
}}
>
<Text variant="b2">{`${cmd.name}${cmd.isOptions ? cmdOptString : ''}`}</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, emos) {
return emos.map((emoji) => (
<CmdItem
key={emoji.hexcode}
onClick={() => fireCmd({
prefix: emPrefix,
result: emoji,
})}
>
{
parse(twemoji.parse(
emoji.unicode,
{
attributes: () => ({
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 cmd = {
'/': (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]?.(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) {
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 });
cmdPrefix = prefix;
const { roomList, matrixClient } = initMatrix;
function getRooms(roomIds) {
return roomIds.map((rId) => {
const room = matrixClient.getRoom(rId);
return { return {
name: room.name, name: room.name,
roomId: room.roomId, roomId: room.roomId,
}; };
}); });
const fuse = new Fuse(rooms, {
includeScore: true,
keys: ['name'],
threshold: '0.3',
});
return fuse.search(term);
} }
const setupSearch = {
function searchCommands(term) { '/': () => asyncSearch.setup(commands, { keys: ['name'], isContain: true }),
const fuse = new Fuse(commands, { '>*': () => asyncSearch.setup(getRooms([...roomList.spaces]), { keys: ['name'], limit: 20 }),
includeScore: true, '>#': () => asyncSearch.setup(getRooms([...roomList.rooms]), { keys: ['name'], limit: 20 }),
keys: ['name'], '>@': () => asyncSearch.setup(getRooms([...roomList.directs]), { keys: ['name'], limit: 20 }),
threshold: '0.3', ':': () => asyncSearch.setup(emojis, { keys: ['shortcode'], limit: 20 }),
}); '@': () => asyncSearch.setup(matrixClient.getRoom(roomId).getJoinedMembers().map((member) => ({
return fuse.search(term); name: member.name,
} userId: member.userId.slice(1),
})), { keys: ['name', 'userId'], limit: 20 }),
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) => ( setupSearch[prefix]?.();
<CmdItem
key={finding.item.roomId}
onClick={() => {
fireCmd({
prefix: cmdPrefix,
slug: roomSlug,
result: finding.item,
});
}}
>
<Text variant="b2">{finding.item.name}</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 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) => (
<CmdItem
key={finding.item.hexcode}
onClick={() => fireCmd({
prefix: emPrefix,
slug: shortcutSlug,
result: finding.item,
})}
>
{
parse(twemoji.parse(
finding.item.unicode,
{
attributes: () => ({
unicode: finding.item.unicode,
shortcodes: finding.item.shortcodes?.toString(),
}),
},
))
}
</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),
};
return cmd[prefix]?.(slug);
}
function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
const [cmd, setCmd] = useState(null);
function processCmd(prefix, slug) {
setCmd({ prefix, slug });
}
function activateCmd(prefix) {
setCmd({ prefix });
perfectMatchCmd = null;
} }
function deactivateCmd() { function deactivateCmd() {
setCmd(null); setCmd(null);
perfectMatchCmd = null; cmdOption = undefined;
cmdPrefix = undefined;
} }
function fireCmd(myCmd) { function fireCmd(myCmd) {
if (myCmd.prefix.match(/^>[*#@]$/)) { if (myCmd.prefix.match(/^>[*#@]$/)) {
@ -364,34 +369,44 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
replace: myCmd.result.unicode, replace: myCmd.result.unicode,
}); });
} }
if (myCmd.prefix === '@') {
viewEvent.emit('cmd_fired', {
replace: myCmd.result.name,
});
}
deactivateCmd(); deactivateCmd();
} }
function executeCmd() { function executeCmd() {
if (perfectMatchCmd === null) return; if (cmd.suggestions.length === 0) return;
if (perfectMatchCmd.result === null) return; fireCmd({
fireCmd(perfectMatchCmd); prefix: cmd.prefix,
} option: cmd.option,
function errorCmd() { result: cmd.suggestions[0],
setCmd({ error: 'No suggestion found.' }); });
} }
useEffect(() => { useEffect(() => {
viewEvent.on('cmd_activate', activateCmd); viewEvent.on('cmd_activate', activateCmd);
viewEvent.on('cmd_process', processCmd);
viewEvent.on('cmd_deactivate', deactivateCmd); viewEvent.on('cmd_deactivate', deactivateCmd);
viewEvent.on('cmd_exe', executeCmd);
viewEvent.on('cmd_error', errorCmd);
return () => { return () => {
deactivateCmd(); deactivateCmd();
viewEvent.removeListener('cmd_activate', activateCmd); viewEvent.removeListener('cmd_activate', activateCmd);
viewEvent.removeListener('cmd_process', processCmd);
viewEvent.removeListener('cmd_deactivate', deactivateCmd); viewEvent.removeListener('cmd_deactivate', deactivateCmd);
viewEvent.removeListener('cmd_exe', executeCmd);
viewEvent.removeListener('cmd_error', errorCmd);
}; };
}, [roomId]); }, [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 ( return (
<div className="cmd-bar"> <div className="cmd-bar">
<div className="cmd-bar__info"> <div className="cmd-bar__info">
@ -408,8 +423,8 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
<div className="cmd-bar"> <div className="cmd-bar">
<div className="cmd-bar__info"> <div className="cmd-bar__info">
{cmd === null && <CmdHelp />} {cmd === null && <CmdHelp />}
{cmd !== null && typeof cmd.slug === 'undefined' && <div className="cmd-bar__info-indicator" /> } {cmd !== null && typeof cmd.suggestions === 'undefined' && <div className="cmd-bar__info-indicator" /> }
{cmd !== null && typeof cmd.slug === 'string' && <Text variant="b3">TAB</Text>} {cmd !== null && typeof cmd.suggestions !== 'undefined' && <Text variant="b3">TAB</Text>}
</div> </div>
<div className="cmd-bar__content"> <div className="cmd-bar__content">
{cmd === null && ( {cmd === null && (
@ -419,10 +434,10 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
viewEvent={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.suggestions === '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' && (
<ScrollView horizontal vertical={false} invisible> <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> </ScrollView>
)} )}
</div> </div>

View file

@ -117,14 +117,10 @@
border-radius: var(--bo-radius) var(--bo-radius) 0 0; border-radius: var(--bo-radius) var(--bo-radius) 0 0;
cursor: pointer; cursor: pointer;
[dir=rtl] & {
margin-right: 0;
margin-left: var(--sp-extra-tight);
}
& .emoji { & .emoji {
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-right: var(--sp-ultra-tight);
} }
&:hover { &:hover {
@ -136,4 +132,13 @@
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
outline: none; outline: none;
} }
[dir=rtl] & {
margin-right: 0;
margin-left: var(--sp-extra-tight);
& .emoji {
margin-right: 0;
margin-left: var(--sp-ultra-tight);
}
}
} }

View file

@ -29,7 +29,7 @@ import MarkdownIC from '../../../../public/res/ic/outlined/markdown.svg';
import FileIC from '../../../../public/res/ic/outlined/file.svg'; import FileIC from '../../../../public/res/ic/outlined/file.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
const CMD_REGEX = /(\/|>[#*@]|:)(\S*)$/; const CMD_REGEX = /(\/|>[#*@]|:|@)(\S*)$/;
let isTyping = false; let isTyping = false;
let isCmdActivated = false; let isCmdActivated = false;
let cmdCursorPos = null; let cmdCursorPos = null;
@ -90,20 +90,26 @@ function ChannelViewInput({
function activateCmd(prefix) { function activateCmd(prefix) {
isCmdActivated = true; isCmdActivated = true;
requestAnimationFrame(() => {
inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-positive)'; inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-positive)';
});
rightOptionsA11Y(false); rightOptionsA11Y(false);
viewEvent.emit('cmd_activate', prefix); viewEvent.emit('cmd_activate', prefix);
} }
function deactivateCmd() { function deactivateCmd() {
if (inputBaseRef.current !== null) { if (inputBaseRef.current !== null) {
requestAnimationFrame(() => {
inputBaseRef.current.style.boxShadow = 'var(--bs-surface-border)'; inputBaseRef.current.style.boxShadow = 'var(--bs-surface-border)';
});
rightOptionsA11Y(true); rightOptionsA11Y(true);
} }
isCmdActivated = false; isCmdActivated = false;
cmdCursorPos = null; cmdCursorPos = null;
} }
function errorCmd() { function errorCmd() {
requestAnimationFrame(() => {
inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-danger)'; inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-danger)';
});
} }
function setCursorPosition(pos) { function setCursorPosition(pos) {
setTimeout(() => { setTimeout(() => {
@ -242,7 +248,9 @@ function ChannelViewInput({
return; return;
} }
if (!isCmdActivated) activateCmd(cmdPrefix); if (!isCmdActivated) activateCmd(cmdPrefix);
requestAnimationFrame(() => {
inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-caution)'; inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-caution)';
});
viewEvent.emit('cmd_process', cmdPrefix, cmdSlug); viewEvent.emit('cmd_process', cmdPrefix, cmdSlug);
} }

View file

@ -53,11 +53,15 @@ function addToGroup(emoji) {
const emojis = []; const emojis = [];
emojisData.forEach((emoji) => { 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); addToGroup(em);
emojis.push(em); emojis.push(em);
}); });
function searchEmoji(term) { function searchEmoji(term) {
const options = { const options = {
includeScore: true, includeScore: true,

View file

@ -7,7 +7,7 @@ class Navigation extends EventEmitter {
super(); super();
this.activeTab = 'channels'; this.activeTab = 'channels';
this.selectedRoom = null; this.activeRoomId = null;
this.isPeopleDrawerVisible = true; this.isPeopleDrawerVisible = true;
} }
@ -15,8 +15,8 @@ class Navigation extends EventEmitter {
return this.activeTab; return this.activeTab;
} }
getActiveRoom() { getActiveRoomId() {
return this.selectedRoom; return this.activeRoomId;
} }
navigate(action) { navigate(action) {
@ -26,8 +26,8 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.TAB_CHANGED, this.activeTab); this.emit(cons.events.navigation.TAB_CHANGED, this.activeTab);
}, },
[cons.actions.navigation.SELECT_ROOM]: () => { [cons.actions.navigation.SELECT_ROOM]: () => {
this.selectedRoom = action.roomId; this.activeRoomId = action.roomId;
this.emit(cons.events.navigation.ROOM_SELECTED, this.selectedRoom); this.emit(cons.events.navigation.ROOM_SELECTED, this.activeRoomId);
}, },
[cons.actions.navigation.TOGGLE_PEOPLE_DRAWER]: () => { [cons.actions.navigation.TOGGLE_PEOPLE_DRAWER]: () => {
this.isPeopleDrawerVisible = !this.isPeopleDrawerVisible; this.isPeopleDrawerVisible = !this.isPeopleDrawerVisible;

View file

@ -82,7 +82,7 @@ class AsyncSearch extends EventEmitter {
if (lastFindingCount !== thisFindingCount) this._sendFindings(); if (lastFindingCount !== thisFindingCount) this._sendFindings();
this.searchUptoIndex = searchIndex + 1; this.searchUptoIndex = searchIndex + 1;
queueMicrotask(() => this._find(thisSessionTimestamp, thisFindingCount)); setTimeout(() => this._find(thisSessionTimestamp, thisFindingCount));
return; return;
} }
} }