cinny/src/app/organisms/room/RoomViewCmdBar.jsx

325 lines
9.8 KiB
React
Raw Normal View History

2021-08-04 09:52:59 +00:00
/* eslint-disable react/prop-types */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
2021-08-31 13:13:31 +00:00
import './RoomViewCmdBar.scss';
2021-08-09 09:13:43 +00:00
import parse from 'html-react-parser';
import twemoji from 'twemoji';
2021-08-04 09:52:59 +00:00
import { twemojify } from '../../../util/twemojify';
2021-08-04 09:52:59 +00:00
import initMatrix from '../../../client/initMatrix';
2021-08-08 16:26:34 +00:00
import { toggleMarkdown } from '../../../client/action/settings';
import * as roomActions from '../../../client/action/room';
import {
2021-08-31 13:13:31 +00:00
openCreateRoom,
openPublicRooms,
2021-08-08 16:26:34 +00:00
openInviteUser,
} from '../../../client/action/navigation';
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
import AsyncSearch from '../../../util/AsyncSearch';
2021-08-04 09:52:59 +00:00
2021-08-08 16:26:34 +00:00
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';
2021-08-04 09:52:59 +00:00
2021-08-08 16:26:34 +00:00
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),
}, {
2021-08-31 13:13:31 +00:00
name: 'createRoom',
description: 'Create new room',
exe: () => openCreateRoom(),
2021-08-08 16:26:34 +00:00
}, {
name: 'join',
isOptions: true,
2021-08-31 13:13:31 +00:00
description: 'Join room with alias. Example: /join/#cinny:matrix.org',
exe: (roomId, searchTerm) => openPublicRooms(searchTerm),
2021-08-08 16:26:34 +00:00
}, {
name: 'leave',
2021-08-31 13:13:31 +00:00
description: 'Leave current room',
Adapt to different device widths (#401) * Now adapting to small screen sizes, needs improvements * Fix that site only gets into mobile mode when resized * - Added navigation event triggered if user requests to return to navigation on compact screens - People drawer wont be shown on compact screens - Still accessible using settings - would be duplicated UI - mobileSize is now compactSize * Put threshold for collapsing the base UI in a shared file * Switch to a more simple solution using CSS media queries over JS - Move back button to the left a bit so it doesnt get in touch with room icon * switch from component-individual-thresholds to device-type thresholds - <750px: Mobile - <900px: Tablet - >900px: Desktop * Make Settings drawer component collapse on mobile * Fix EmojiBoard not showing up and messing up UI when screen is smaller than 360px * Improve code quality; allow passing classNames to IconButton - remove unnessesary div wrappers - use dir.side where appropriate - rename threshold and its mixins to more descriptive names - Rename "OPEN_NAVIGATION" to "NAVIGATION_OPENED" * - follow BEM methology - remove ROOM_SELECTED listener - rename NAVIGATION_OPENED to OPEN_NAVIGATION where appropriate - this does NOT changes that ref should be used for changing visability * Use ref to change visability to avoid re-rendering * Use ref to change visability to avoid re-rendering * Fix that room component is not hidden by default. This resulted in a broken view when application is viewed in mobile size without having selected a room since loading. * fix: leaving a room should bring one back to navigation Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
2022-04-24 10:23:10 +00:00
exe: (roomId) => {
roomActions.leave(roomId);
},
2021-08-08 16:26:34 +00:00
}, {
name: 'invite',
isOptions: true,
description: 'Invite user to room. Example: /invite/@johndoe:matrix.org',
exe: (roomId, searchTerm) => openInviteUser(roomId, searchTerm),
}];
function CmdItem({ onClick, children }) {
2021-08-04 09:52:59 +00:00
return (
2021-08-08 16:26:34 +00:00
<button className="cmd-item" onClick={onClick} type="button">
{children}
</button>
);
}
CmdItem.propTypes = {
onClick: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
function renderCmdSuggestions(cmdPrefix, cmds) {
const cmdOptString = (typeof option === 'string') ? `/${option}` : '/?';
return cmds.map((cmd) => (
2021-08-08 16:26:34 +00:00
<CmdItem
key={cmd.name}
2021-08-08 16:26:34 +00:00
onClick={() => {
fireCmd({
prefix: cmdPrefix,
option,
result: cmd,
2021-08-08 16:26:34 +00:00
});
}}
>
<Text variant="b2">{`${cmd.name}${cmd.isOptions ? cmdOptString : ''}`}</Text>
2021-08-08 16:26:34 +00:00
</CmdItem>
));
}
function renderEmojiSuggestion(emPrefix, emos) {
const mx = initMatrix.matrixClient;
// Renders a small Twemoji
function renderTwemoji(emoji) {
return parse(twemoji.parse(
emoji.unicode,
{
attributes: () => ({
unicode: emoji.unicode,
shortcodes: emoji.shortcodes?.toString(),
}),
},
));
}
// Render a custom emoji
function renderCustomEmoji(emoji) {
return (
<img
className="emoji"
src={mx.mxcUrlToHttp(emoji.mxc)}
data-mx-emoticon=""
alt={`:${emoji.shortcode}:`}
/>
);
}
// Dynamically render either a custom emoji or twemoji based on what the input is
function renderEmoji(emoji) {
if (emoji.mxc) {
return renderCustomEmoji(emoji);
}
return renderTwemoji(emoji);
}
return emos.map((emoji) => (
2021-08-08 16:26:34 +00:00
<CmdItem
key={emoji.shortcode}
2021-08-08 16:26:34 +00:00
onClick={() => fireCmd({
prefix: emPrefix,
result: emoji,
2021-08-08 16:26:34 +00:00
})}
>
<Text variant="b1">{renderEmoji(emoji)}</Text>
<Text variant="b2">{`:${emoji.shortcode}:`}</Text>
</CmdItem>
));
}
function renderNameSuggestion(namePrefix, members) {
return members.map((member) => (
<CmdItem
key={member.userId}
onClick={() => {
fireCmd({
prefix: namePrefix,
result: member,
});
}}
>
<Text variant="b2">{twemojify(member.name)}</Text>
2021-08-08 16:26:34 +00:00
</CmdItem>
));
}
const cmd = {
'/': (cmds) => renderCmdSuggestions(prefix, cmds),
':': (emos) => renderEmojiSuggestion(prefix, emos),
'@': (members) => renderNameSuggestion(prefix, members),
2021-08-08 16:26:34 +00:00
};
return cmd[prefix]?.(suggestions);
2021-08-08 16:26:34 +00:00
}
const asyncSearch = new AsyncSearch();
let cmdPrefix;
let cmdOption;
2021-08-31 13:13:31 +00:00
function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
2021-08-08 16:26:34 +00:00
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 });
}
2021-08-08 16:26:34 +00:00
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';
else if (searchTerm.match(/^(c|ca|cat)$/)) searchTerm = '_cat';
}
}
asyncSearch.search(searchTerm);
2021-08-08 16:26:34 +00:00
}
function activateCmd(prefix) {
cmdPrefix = prefix;
cmdPrefix = undefined;
const mx = initMatrix.matrixClient;
const setupSearch = {
'/': () => {
asyncSearch.setup(commands, { keys: ['name'], isContain: true });
setCmd({ prefix, suggestions: commands });
},
':': () => {
const emojis = getEmojiForCompletion(mx.getRoom(roomId));
asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 20 });
setCmd({ prefix, suggestions: emojis.slice(26, 46) });
},
'@': () => {
const members = mx.getRoom(roomId).getJoinedMembers().map((member) => ({
name: member.name,
userId: member.userId.slice(1),
}));
asyncSearch.setup(members, { keys: ['name', 'userId'], limit: 20 });
const endIndex = members.length > 20 ? 20 : members.length;
setCmd({ prefix, suggestions: members.slice(0, endIndex) });
},
};
setupSearch[prefix]?.();
2021-08-08 16:26:34 +00:00
}
function deactivateCmd() {
setCmd(null);
cmdOption = undefined;
cmdPrefix = undefined;
2021-08-08 16:26:34 +00:00
}
function fireCmd(myCmd) {
if (myCmd.prefix === '/') {
myCmd.result.exe(roomId, myCmd.option);
viewEvent.emit('cmd_fired');
}
if (myCmd.prefix === ':') {
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
2021-08-08 16:26:34 +00:00
viewEvent.emit('cmd_fired', {
replace: myCmd.result.mxc ? `:${myCmd.result.shortcode}: ` : myCmd.result.unicode,
2021-08-08 16:26:34 +00:00
});
}
if (myCmd.prefix === '@') {
viewEvent.emit('cmd_fired', {
replace: myCmd.result.name,
});
}
2021-08-08 16:26:34 +00:00
deactivateCmd();
}
function listenKeyboard(event) {
const { activeElement } = document;
const lastCmdItem = document.activeElement.parentNode.lastElementChild;
if (event.key === 'Escape') {
if (activeElement.className !== 'cmd-item') return;
viewEvent.emit('focus_msg_input');
}
if (event.key === 'Tab') {
if (lastCmdItem.className !== 'cmd-item') return;
if (lastCmdItem !== activeElement) return;
if (event.shiftKey) return;
viewEvent.emit('focus_msg_input');
event.preventDefault();
}
}
2021-08-08 16:26:34 +00:00
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);
asyncSearch.on(asyncSearch.RESULT_SENT, displaySuggestions);
return () => {
if (cmd !== null) document.body.removeEventListener('keydown', listenKeyboard);
viewEvent.removeListener('cmd_process', processCmd);
asyncSearch.removeListener(asyncSearch.RESULT_SENT, displaySuggestions);
};
}, [cmd]);
const isError = typeof cmd?.error === 'string';
if (cmd === null || isError) {
2021-08-08 16:26:34 +00:00
return (
<div className="cmd-bar">
<FollowingMembers roomTimeline={roomTimeline} />
2021-08-08 16:26:34 +00:00
</div>
);
}
return (
<div className="cmd-bar">
<div className="cmd-bar__info">
<Text variant="b3">TAB</Text>
2021-08-08 16:26:34 +00:00
</div>
<div className="cmd-bar__content">
<ScrollView horizontal vertical={false} invisible>
<div className="cmd-bar__content-suggestions">
{ renderSuggestions(cmd, fireCmd) }
</div>
</ScrollView>
2021-08-08 16:26:34 +00:00
</div>
2021-08-04 09:52:59 +00:00
</div>
);
}
2021-08-31 13:13:31 +00:00
RoomViewCmdBar.propTypes = {
2021-08-04 09:52:59 +00:00
roomId: PropTypes.string.isRequired,
roomTimeline: PropTypes.shape({}).isRequired,
viewEvent: PropTypes.shape({}).isRequired,
};
2021-08-31 13:13:31 +00:00
export default RoomViewCmdBar;