/* eslint-disable react/prop-types */ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import './RoomViewCmdBar.scss'; import parse from 'html-react-parser'; import twemoji from 'twemoji'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import { toggleMarkdown } from '../../../client/action/settings'; import * as roomActions from '../../../client/action/room'; import { selectTab, selectRoom, openCreateRoom, openPublicRooms, openInviteUser, openReadReceipts, } from '../../../client/action/navigation'; import { emojis } from '../emoji-board/emoji'; import AsyncSearch from '../../../util/AsyncSearch'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; import IconButton from '../../atoms/button/IconButton'; import ContextMenu, { MenuHeader } from '../../atoms/context-menu/ContextMenu'; import ScrollView from '../../atoms/scroll/ScrollView'; import SettingTile from '../../molecules/setting-tile/SettingTile'; import TimelineChange from '../../molecules/message/TimelineChange'; import CmdIC from '../../../../public/res/ic/outlined/cmd.svg'; import { getUsersActionJsx } from './common'; const commands = [{ name: 'markdown', description: 'Toggle markdown for messages.', exe: () => toggleMarkdown(), }, { name: 'startDM', isOptions: true, description: 'Start direct message with user. Example: /startDM/@johndoe.matrix.org', exe: (roomId, searchTerm) => openInviteUser(undefined, searchTerm), }, { name: 'createRoom', description: 'Create new room', exe: () => openCreateRoom(), }, { name: 'join', isOptions: true, description: 'Join room with alias. Example: /join/#cinny:matrix.org', exe: (roomId, searchTerm) => openPublicRooms(searchTerm), }, { name: 'leave', description: 'Leave current room', exe: (roomId) => roomActions.leave(roomId), }, { name: 'invite', isOptions: true, description: 'Invite user to room. Example: /invite/@johndoe:matrix.org', exe: (roomId, searchTerm) => openInviteUser(roomId, searchTerm), }]; function CmdHelp() { return ( General command /command_name Go-to commands {'>*space_name'} {'>#room_name'} {'>@people_name'} Autofill commands :emoji_name @name )} render={(toggleMenu) => ( )} /> ); } function ViewCmd() { function renderAllCmds() { return commands.map((command) => ( {command.description})} /> )); } return ( General commands {renderAllCmds()} )} render={(toggleMenu) => ( )} /> ); } function FollowingMembers({ roomId, roomTimeline, viewEvent }) { const [followingMembers, setFollowingMembers] = useState([]); const mx = initMatrix.matrixClient; function handleOnMessageSent() { setFollowingMembers([]); } function updateFollowingMembers() { const room = mx.getRoom(roomId); const { timeline } = room; const userIds = room.getUsersReadUpTo(timeline[timeline.length - 1]); const myUserId = mx.getUserId(); setFollowingMembers(userIds.filter((userId) => userId !== myUserId)); } useEffect(() => updateFollowingMembers(), [roomId]); useEffect(() => { roomTimeline.on(cons.events.roomTimeline.READ_RECEIPT, updateFollowingMembers); viewEvent.on('message_sent', handleOnMessageSent); return () => { roomTimeline.removeListener(cons.events.roomTimeline.READ_RECEIPT, updateFollowingMembers); viewEvent.removeListener('message_sent', handleOnMessageSent); }; }, [roomTimeline]); const lastMEvent = roomTimeline.timeline[roomTimeline.timeline.length - 1]; return followingMembers.length !== 0 && ( openReadReceipts(roomId, lastMEvent.getId())} /> ); } FollowingMembers.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, viewEvent: PropTypes.shape({}).isRequired, }; function getCmdActivationMessage(prefix) { function genMessage(prime, secondary) { return ( <> {prime} {secondary} ); } const cmd = { '/': () => genMessage('General command mode activated. ', 'Type command name for suggestions.'), '>*': () => genMessage('Go-to command mode activated. ', 'Type space name for suggestions.'), '>#': () => genMessage('Go-to command mode activated. ', 'Type room 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]?.(); } function CmdItem({ onClick, children }) { return ( ); } CmdItem.propTypes = { onClick: PropTypes.func.isRequired, children: PropTypes.node.isRequired, }; function getCmdSuggestions({ prefix, option, suggestions }, fireCmd) { function getGenCmdSuggestions(cmdPrefix, cmds) { const cmdOptString = (typeof option === 'string') ? `/${option}` : '/?'; return cmds.map((cmd) => ( { fireCmd({ prefix: cmdPrefix, option, result: cmd, }); }} > {`${cmd.name}${cmd.isOptions ? cmdOptString : ''}`} )); } function getRoomsSuggestion(cmdPrefix, rooms) { return rooms.map((room) => ( { fireCmd({ prefix: cmdPrefix, result: room, }); }} > {room.name} )); } function getEmojiSuggestion(emPrefix, emos) { return emos.map((emoji) => ( fireCmd({ prefix: emPrefix, result: emoji, })} > { parse(twemoji.parse( emoji.unicode, { attributes: () => ({ 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 cmd = { '/': (cmds) => getGenCmdSuggestions(prefix, cmds), '>*': (spaces) => getRoomsSuggestion(prefix, spaces), '>#': (rooms) => getRoomsSuggestion(prefix, rooms), '>@': (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 RoomViewCmdBar({ 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 = 'stuck_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 { 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); cmdOption = undefined; cmdPrefix = undefined; } function fireCmd(myCmd) { if (myCmd.prefix.match(/^>[*#@]$/)) { if (cmd.prefix === '>*') selectTab(myCmd.result.roomId); else selectRoom(myCmd.result.roomId); viewEvent.emit('cmd_fired'); } if (myCmd.prefix === '/') { myCmd.result.exe(roomId, myCmd.option); viewEvent.emit('cmd_fired'); } if (myCmd.prefix === ':') { viewEvent.emit('cmd_fired', { replace: myCmd.result.unicode, }); } if (myCmd.prefix === '@') { viewEvent.emit('cmd_fired', { replace: myCmd.result.name, }); } deactivateCmd(); } function executeCmd() { if (cmd.suggestions.length === 0) return; fireCmd({ prefix: cmd.prefix, option: cmd.option, result: cmd.suggestions[0], }); } function listenKeyboard(event) { const { activeElement } = document; const lastCmdItem = document.activeElement.parentNode.lastElementChild; if (event.keyCode === 27) { if (activeElement.className !== 'cmd-item') return; viewEvent.emit('focus_msg_input'); } if (event.keyCode === 9) { if (lastCmdItem.className !== 'cmd-item') return; if (lastCmdItem !== activeElement) return; if (event.shiftKey) return; viewEvent.emit('focus_msg_input'); event.preventDefault(); } } useEffect(() => { viewEvent.on('cmd_activate', activateCmd); viewEvent.on('cmd_deactivate', deactivateCmd); return () => { deactivateCmd(); viewEvent.removeListener('cmd_activate', activateCmd); viewEvent.removeListener('cmd_deactivate', deactivateCmd); }; }, [roomId]); useEffect(() => { if (cmd !== null) document.body.addEventListener('keydown', listenKeyboard); viewEvent.on('cmd_process', processCmd); viewEvent.on('cmd_exe', executeCmd); asyncSearch.on(asyncSearch.RESULT_SENT, displaySuggestions); return () => { if (cmd !== null) document.body.removeEventListener('keydown', listenKeyboard); viewEvent.removeListener('cmd_process', processCmd); viewEvent.removeListener('cmd_exe', executeCmd); asyncSearch.removeListener(asyncSearch.RESULT_SENT, displaySuggestions); }; }, [cmd]); if (typeof cmd?.error === 'string') { return (
{cmd.error}
); } return (
{cmd === null && } {cmd !== null && typeof cmd.suggestions === 'undefined' &&
} {cmd !== null && typeof cmd.suggestions !== 'undefined' && TAB}
{cmd === null && ( )} {cmd !== null && typeof cmd.suggestions === 'undefined' && {getCmdActivationMessage(cmd.prefix)}} {cmd !== null && typeof cmd.suggestions !== 'undefined' && (
{getCmdSuggestions(cmd, fireCmd)}
)}
{cmd !== null && cmd.prefix === '/' && }
); } RoomViewCmdBar.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, viewEvent: PropTypes.shape({}).isRequired, }; export default RoomViewCmdBar;