--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<g>
+ <path d="M20,4H4C2.9,4,2,4.9,2,6v12c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V6C22,4.9,21.1,4,20,4z M20,18H4V6h16V18z"/>
+ <polygon points="7.5,16.5 12.1,12 7.5,7.5 6.5,8.5 9.9,12 6.5,15.5 "/>
+ <rect x="13" y="14.5" width="5" height="1.5"/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<g>
+ <polygon points="12,7 12,7 8,11 4,7 4,7 4,7 2,7 2,17 4,17 4,9.8 8,13.8 12,9.8 12,17 14,17 14,7 12,7 "/>
+ <path d="M20,14V7h-2v7h-2l3,3c0.1,0,0.5-0.4,1-0.9c0.9-0.9,2-2.1,2-2.1H20z"/>
+</g>
+</svg>
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './ChannelViewCmdBar.scss';
+import Fuse from 'fuse.js';
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 {
+ selectRoom,
+ openCreateChannel,
+ openPublicChannels,
+ openInviteUser,
+} from '../../../client/action/navigation';
+import { searchEmoji } from '../emoji-board/emoji';
+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';
-function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
+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: 'createChannel',
+ description: 'Create new channel',
+ exe: () => openCreateChannel(),
+}, {
+ name: 'join',
+ isOptions: true,
+ description: 'Join channel with alias. Example: /join/#cinny:matrix.org',
+ exe: (roomId, searchTerm) => openPublicChannels(searchTerm),
+}, {
+ name: 'leave',
+ description: 'Leave current channel',
+ 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 (
+ <ContextMenu
+ placement="top"
+ content={(
+ <>
+ <MenuHeader>General command</MenuHeader>
+ <Text variant="b2">/command_name</Text>
+ <MenuHeader>Go-to commands</MenuHeader>
+ <Text variant="b2">{'>*space_name'}</Text>
+ <Text variant="b2">{'>#channel_name'}</Text>
+ <Text variant="b2">{'>@people_name'}</Text>
+ <MenuHeader>Autofill command</MenuHeader>
+ <Text variant="b2">:emoji_name:</Text>
+ </>
+ )}
+ render={(toggleMenu) => (
+ <IconButton
+ src={CmdIC}
+ size="extra-small"
+ onClick={toggleMenu}
+ tooltip="Commands"
+ />
+ )}
+ />
+ );
+}
+
+function ViewCmd() {
+ function renderAllCmds() {
+ return commands.map((command) => (
+ <SettingTile
+ key={command.name}
+ title={command.name}
+ content={(<Text variant="b3">{command.description}</Text>)}
+ />
+ ));
+ }
+ return (
+ <ContextMenu
+ maxWidth={250}
+ placement="top"
+ content={(
+ <>
+ <MenuHeader>General commands</MenuHeader>
+ {renderAllCmds()}
+ </>
+ )}
+ render={(toggleMenu) => (
+ <span>
+ <Button onClick={toggleMenu}><span className="text text-b3">View all</span></Button>
+ </span>
+ )}
+ />
+ );
+}
+
+function FollowingMembers({ roomId, roomTimeline, viewEvent }) {
const [followingMembers, setFollowingMembers] = useState([]);
const mx = initMatrix.matrixClient;
setFollowingMembers(userIds.filter((userId) => userId !== myUserId));
}
- useEffect(() => {
- updateFollowingMembers();
- }, [roomId]);
+ useEffect(() => updateFollowingMembers(), [roomId]);
useEffect(() => {
roomTimeline.on(cons.events.roomTimeline.READ_RECEIPT, updateFollowingMembers);
};
}, [roomTimeline]);
+ return followingMembers.length !== 0 && (
+ <TimelineChange
+ variant="follow"
+ content={getUsersActionJsx(followingMembers, 'following the conversation.')}
+ time=""
+ />
+ );
+}
+
+FollowingMembers.propTypes = {
+ roomId: PropTypes.string.isRequired,
+ roomTimeline: PropTypes.shape({}).isRequired,
+ viewEvent: PropTypes.shape({}).isRequired,
+};
+
+function getCmdActivationMessage(prefix) {
+ function genMessage(prime, secondary) {
+ return (
+ <>
+ <span>{prime}</span>
+ <span>{secondary}</span>
+ </>
+ );
+ }
+ 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 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.'),
+ };
+ return cmd[prefix]?.();
+}
+
+function CmdItem({ onClick, children }) {
return (
- <div className="channel-cmd-bar">
- {
- followingMembers.length !== 0 && (
- <TimelineChange
- variant="follow"
- content={getUsersActionJsx(followingMembers, 'following the conversation.')}
- time=""
- />
- )
+ <button className="cmd-item" onClick={onClick} type="button">
+ {children}
+ </button>
+ );
+}
+CmdItem.propTypes = {
+ onClick: PropTypes.func.isRequired,
+ 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) => (
+ <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) {
+ const result = searchEmoji(shortcutSlug);
+ 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,
+ })}
+ >
+ <Text variant="b2">{finding.item.unicode}</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),
+ };
+ 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() {
+ setCmd(null);
+ perfectMatchCmd = null;
+ }
+ function fireCmd(myCmd) {
+ if (myCmd.prefix.match(/^>[*#@]$/)) {
+ 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,
+ });
+ }
+ deactivateCmd();
+ }
+ function executeCmd() {
+ if (perfectMatchCmd === null) return;
+ if (perfectMatchCmd.result === null) return;
+ fireCmd(perfectMatchCmd);
+ }
+ function errorCmd() {
+ setCmd({ error: 'No suggestion found.' });
+ }
+
+ 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') {
+ return (
+ <div className="cmd-bar">
+ <div className="cmd-bar__info">
+ <div className="cmd-bar__info-indicator--error" />
+ </div>
+ <div className="cmd-bar__content">
+ <Text className="cmd-bar__content-error" variant="b2">{cmd.error}</Text>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <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>}
+ </div>
+ <div className="cmd-bar__content">
+ {cmd === null && (
+ <FollowingMembers
+ roomId={roomId}
+ roomTimeline={roomTimeline}
+ 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' && (
+ <ScrollView horizontal vertical={false} invisible>
+ <div className="cmd-bar__content__suggestions">{getCmdSuggestions(cmd, fireCmd, viewEvent)}</div>
+ </ScrollView>
+ )}
+ </div>
+ <div className="cmd-bar__more">
+ {cmd !== null && cmd.prefix === '/' && <ViewCmd />}
+ </div>
</div>
);
}
-.channel-cmd-bar {
+.overflow-ellipsis {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.cmd-bar {
--cmd-bar-height: 28px;
min-height: var(--cmd-bar-height);
+ display: flex;
+
+ &__info {
+ display: flex;
+ width: calc(2 * var(--sp-extra-loose));
+ padding-left: var(--sp-ultra-tight);
+ [dir=rtl] & {
+ padding-left: 0;
+ padding-right: var(--sp-ultra-tight);
+ }
+
+ & > * {
+ margin: auto;
+ }
+
+ & .ic-btn-surface {
+ padding: 0;
+ & .ic-raw {
+ background-color: var(--tc-surface-low);
+ }
+ }
+ & .context-menu .text-b2 {
+ margin: var(--sp-extra-tight) var(--sp-tight);
+ }
+
+ &-indicator,
+ &-indicator--error {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--bg-positive);
+ }
+ &-indicator--error {
+ background-color: var(--bg-danger);
+ }
+ }
+
+ &__content {
+ min-width: 0;
+ flex: 1;
+ display: flex;
+
+ &-help,
+ &-error {
+ @extend .overflow-ellipsis;
+ align-self: center;
+ span {
+ color: var(--tc-surface-low);
+ &:first-child {
+ color: var(--tc-surface-normal)
+ }
+ }
+ }
+ &-error {
+ color: var(--bg-danger);
+ }
+ &__suggestions {
+ display: flex;
+ height: 100%;
+ white-space: nowrap;
+ }
+ }
+ &__more {
+ display: flex;
+ & button {
+ min-width: 0;
+ height: 100%;
+ margin: 0 var(--sp-normal);
+ padding: 0 var(--sp-extra-tight);
+ box-shadow: none;
+ border-radius: var(--bo-radius) var(--bo-radius) 0 0;
+ & .text {
+ color: var(--tc-surface-normal);
+ }
+ }
+ & .setting-tile {
+ margin: var(--sp-tight);
+ }
+ }
& .timeline-change {
+ width: 100%;
justify-content: flex-end;
padding: var(--sp-ultra-tight) var(--sp-normal);
+ border-radius: var(--bo-radius) var(--bo-radius) 0 0;
&__content {
margin: 0;
flex: unset;
& > .text {
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
+ @extend .overflow-ellipsis;
& b {
color: var(--tc-surface-normal);
}
}
}
}
+}
+
+.cmd-item {
+ --cmd-item-bar: inset 0 -2px 0 0 var(--bg-caution);
+
+ display: inline-flex;
+ align-items: center;
+ margin-right: var(--sp-extra-tight);
+ padding: 0 var(--sp-extra-tight);
+ height: 100%;
+ border-radius: var(--bo-radius) var(--bo-radius) 0 0;
+ cursor: pointer;
+
+ [dir=rtl] & {
+ margin-right: 0;
+ margin-left: var(--sp-extra-tight);
+ }
+
+ &:hover {
+ background-color: var(--bg-caution-hover);
+ }
+ &:focus {
+ background-color: var(--bg-caution-hover);
+ box-shadow: var(--cmd-item-bar);
+ border-bottom: 2px solid transparent;
+ outline: none;
+ }
}
\ No newline at end of file
const mx = initMatrix.matrixClient;
const avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
const roomName = mx.getRoom(roomId).name;
- const isDM = initMatrix.roomList.directs.has(roomId);
const roomTopic = mx.getRoom(roomId).currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
return (
>
Invite
</MenuItem>
- <MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={() => roomActions.leave(roomId, isDM)}>Leave</MenuItem>
+ <MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={() => roomActions.leave(roomId)}>Leave</MenuItem>
</>
)}
render={(toggleMenu) => <IconButton onClick={toggleMenu} tooltip="Options" src={VerticalMenuIC} />}
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
+import settings from '../../../client/state/settings';
import { bytesToSize } from '../../../util/common';
import Text from '../../atoms/text/Text';
import ShieldIC from '../../../../public/res/ic/outlined/shield.svg';
import VLCIC from '../../../../public/res/ic/outlined/vlc.svg';
import VolumeFullIC from '../../../../public/res/ic/outlined/volume-full.svg';
+import MarkdownIC from '../../../../public/res/ic/outlined/markdown.svg';
import FileIC from '../../../../public/res/ic/outlined/file.svg';
+const CMD_REGEX = /(\/|>[#*@]|:)(\S*)$/;
let isTyping = false;
+let isCmdActivated = false;
+let cmdCursorPos = null;
function ChannelViewInput({
roomId, roomTimeline, timelineScroll, viewEvent,
}) {
const [attachment, setAttachment] = useState(null);
+ const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown);
const textAreaRef = useRef(null);
const inputBaseRef = useRef(null);
const uploadInputRef = useRef(null);
const uploadProgressRef = useRef(null);
+ const rightOptionsRef = useRef(null);
const TYPING_TIMEOUT = 5000;
const mx = initMatrix.matrixClient;
const { roomsInput } = initMatrix;
+ useEffect(() => {
+ settings.on(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
+ return () => {
+ settings.removeListener(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
+ };
+ }, []);
+
const sendIsTyping = (isT) => {
mx.sendTyping(roomId, isT, isT ? TYPING_TIMEOUT : undefined);
isTyping = isT;
uploadInputRef.current.value = null;
}
+ function rightOptionsA11Y(A11Y) {
+ const rightOptions = rightOptionsRef.current.children;
+ for (let index = 0; index < rightOptions.length; index += 1) {
+ rightOptions[index].disabled = !A11Y;
+ }
+ }
+
+ function activateCmd(prefix) {
+ isCmdActivated = true;
+ 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)';
+ rightOptionsA11Y(true);
+ }
+ isCmdActivated = false;
+ cmdCursorPos = null;
+ }
+ function errorCmd() {
+ inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-danger)';
+ }
+ function setCursorPosition(pos) {
+ setTimeout(() => {
+ textAreaRef.current.focus();
+ textAreaRef.current.setSelectionRange(pos, pos);
+ }, 0);
+ }
+ function replaceCmdWith(msg, cursor, replacement) {
+ if (msg === null) return null;
+ const targetInput = msg.slice(0, cursor);
+ const cmdParts = targetInput.match(CMD_REGEX);
+ const leadingInput = msg.slice(0, cmdParts.index);
+ if (replacement.length > 0) setCursorPosition(leadingInput.length + replacement.length);
+ return leadingInput + replacement + msg.slice(cursor);
+ }
+ function firedCmd(cmdData) {
+ const msg = textAreaRef.current.value;
+ textAreaRef.current.value = replaceCmdWith(
+ msg, cmdCursorPos, typeof cmdData?.replace !== 'undefined' ? cmdData.replace : '',
+ );
+ deactivateCmd();
+ }
+
useEffect(() => {
roomsInput.on(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
roomsInput.on(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
+ viewEvent.on('cmd_error', errorCmd);
+ viewEvent.on('cmd_fired', firedCmd);
if (textAreaRef?.current !== null) {
isTyping = false;
textAreaRef.current.focus();
roomsInput.removeListener(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
roomsInput.removeListener(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
+ viewEvent.removeListener('cmd_error', errorCmd);
+ viewEvent.removeListener('cmd_fired', firedCmd);
+ if (isCmdActivated) deactivateCmd();
if (textAreaRef?.current === null) return;
const msg = textAreaRef.current.value;
}, [roomId]);
async function sendMessage() {
+ if (isCmdActivated) {
+ viewEvent.emit('cmd_exe');
+ return;
+ }
+
const msgBody = textAreaRef.current.value;
if (roomsInput.isSending(roomId)) return;
if (msgBody.trim() === '' && attachment === null) return;
}
}
+ function getCursorPosition() {
+ return textAreaRef.current.selectionStart;
+ }
+
+ function recognizeCmd(rawInput) {
+ const cursor = getCursorPosition();
+ const targetInput = rawInput.slice(0, cursor);
+
+ const cmdParts = targetInput.match(CMD_REGEX);
+ if (cmdParts === null) {
+ if (isCmdActivated) {
+ deactivateCmd();
+ viewEvent.emit('cmd_deactivate');
+ }
+ return;
+ }
+ const cmdPrefix = cmdParts[1];
+ const cmdSlug = cmdParts[2];
+
+ cmdCursorPos = cursor;
+ if (cmdSlug === '') {
+ activateCmd(cmdPrefix);
+ return;
+ }
+ if (!isCmdActivated) activateCmd(cmdPrefix);
+ inputBaseRef.current.style.boxShadow = '0 0 0 1px var(--bg-caution)';
+ viewEvent.emit('cmd_process', cmdPrefix, cmdSlug);
+ }
+
function handleMsgTyping(e) {
const msg = e.target.value;
- processTyping(msg);
+ recognizeCmd(e.target.value);
+ if (!isCmdActivated) processTyping(msg);
}
function handleKeyDown(e) {
/>
</Text>
</ScrollView>
+ {isMarkdown && <RawIcon size="extra-small" src={MarkdownIC} />}
</div>
- <div className="channel-input__option-container">
+ <div ref={rightOptionsRef} className="channel-input__option-container">
<ContextMenu
placement="top"
content={(
& > .ic-raw {
transform: scale(0.8);
- margin-left: var(--sp-extra-tight);
- [dir=rtl] & {
- margin-left: 0;
- margin-right: var(--sp-extra-tight);
- }
+ margin: 0 var(--sp-extra-tight);
}
& .scrollbar {
max-height: 50vh;
+ flex: 1;
+
+ &:first-child {
+ margin-left: var(--sp-tight);
+ [dir=rtl] & {
+ margin-left: 0;
+ margin-right: var(--sp-tight);
+ }
+ }
}
}
width: 100%;
min-width: 0;
min-height: 100%;
- padding: var(--sp-ultra-tight) calc(var(--sp-tight) - 2px);
+ padding: var(--sp-ultra-tight) 0;
&::placeholder {
color: var(--tc-surface-low);
setSearchedEmojis([]);
return;
}
- setSearchedEmojis(searchEmoji(term));
+ setSearchedEmojis(searchEmoji(term).map((finding) => finding.item));
}
useEffect(() => {
let result = fuse.search(term);
if (result.length > 20) result = result.slice(0, 20);
- return result.map((finding) => finding.item);
+ return result;
}
export {
--- /dev/null
+import appDispatcher from '../dispatcher';
+import cons from '../state/cons';
+
+function toggleMarkdown() {
+ appDispatcher.dispatch({
+ type: cons.actions.settings.TOGGLE_MARKDOWN,
+ });
+}
+
+export {
+ toggleMarkdown,
+};
LEAVE: 'LEAVE',
CREATE: 'CREATE',
error: {
- CREATE: 'CREATE',
+ CREATE: 'ERROR_CREATE',
},
},
+ settings: {
+ TOGGLE_MARKDOWN: 'TOGGLE_MARKDOWN',
+ },
},
events: {
navigation: {
FILE_UPLOAD_CANCELED: 'FILE_UPLOAD_CANCELED',
ATTACHMENT_CANCELED: 'ATTACHMENT_CANCELED',
},
+ settings: {
+ MARKDOWN_TOGGLED: 'MARKDOWN_TOGGLED',
+ },
},
};
-class Settings {
+import EventEmitter from 'events';
+import appDispatcher from '../dispatcher';
+
+import cons from './cons';
+
+function getSettings() {
+ const settings = localStorage.getItem('settings');
+ if (settings === null) return null;
+ return JSON.parse(settings);
+}
+
+function setSettings(key, value) {
+ let settings = getSettings();
+ if (settings === null) settings = {};
+ settings[key] = value;
+ localStorage.setItem('settings', JSON.stringify(settings));
+}
+
+class Settings extends EventEmitter {
constructor() {
+ super();
+
this.themes = ['', 'silver-theme', 'dark-theme', 'butter-theme'];
this.themeIndex = this.getThemeIndex();
+
+ this.isMarkdown = this.getIsMarkdown();
}
getThemeIndex() {
if (typeof this.themeIndex === 'number') return this.themeIndex;
- let settings = localStorage.getItem('settings');
+ const settings = getSettings();
if (settings === null) return 0;
- settings = JSON.parse(settings);
if (typeof settings.themeIndex === 'undefined') return 0;
// eslint-disable-next-line radix
return parseInt(settings.themeIndex);
appBody.classList.remove(themeName);
});
if (this.themes[themeIndex] !== '') appBody.classList.add(this.themes[themeIndex]);
- localStorage.setItem('settings', JSON.stringify({ themeIndex }));
+ setSettings('themeIndex', themeIndex);
this.themeIndex = themeIndex;
}
+
+ getIsMarkdown() {
+ if (typeof this.isMarkdown === 'boolean') return this.isMarkdown;
+
+ const settings = getSettings();
+ if (settings === null) return false;
+ if (typeof settings.isMarkdown === 'undefined') return false;
+ return settings.isMarkdown;
+ }
+
+ setter(action) {
+ const actions = {
+ [cons.actions.settings.TOGGLE_MARKDOWN]: () => {
+ this.isMarkdown = !this.isMarkdown;
+ setSettings('isMarkdown', this.isMarkdown);
+ this.emit(cons.events.settings.MARKDOWN_TOGGLED, this.isMarkdown);
+ },
+ };
+
+ actions[action.type]?.();
+ }
}
const settings = new Settings();
+appDispatcher.register(settings.setter.bind(settings));
export default settings;