added commands support
This commit is contained in:
parent
0feb56cb3e
commit
b552e2cda8
12 changed files with 682 additions and 34 deletions
11
public/res/ic/outlined/cmd.svg
Normal file
11
public/res/ic/outlined/cmd.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?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>
|
After Width: | Height: | Size: 688 B |
10
public/res/ic/outlined/markdown.svg
Normal file
10
public/res/ic/outlined/markdown.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?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>
|
After Width: | Height: | Size: 637 B |
|
@ -2,15 +2,119 @@
|
||||||
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 initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
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 TimelineChange from '../../molecules/message/TimelineChange';
|
||||||
|
|
||||||
|
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
|
||||||
|
|
||||||
import { getUsersActionJsx } from './common';
|
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 [followingMembers, setFollowingMembers] = useState([]);
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
|
@ -26,9 +130,7 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||||
setFollowingMembers(userIds.filter((userId) => userId !== myUserId));
|
setFollowingMembers(userIds.filter((userId) => userId !== myUserId));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => updateFollowingMembers(), [roomId]);
|
||||||
updateFollowingMembers();
|
|
||||||
}, [roomId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
roomTimeline.on(cons.events.roomTimeline.READ_RECEIPT, updateFollowingMembers);
|
roomTimeline.on(cons.events.roomTimeline.READ_RECEIPT, updateFollowingMembers);
|
||||||
|
@ -39,17 +141,264 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) {
|
||||||
};
|
};
|
||||||
}, [roomTimeline]);
|
}, [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 (
|
return (
|
||||||
<div className="channel-cmd-bar">
|
<button className="cmd-item" onClick={onClick} type="button">
|
||||||
{
|
{children}
|
||||||
followingMembers.length !== 0 && (
|
</button>
|
||||||
<TimelineChange
|
);
|
||||||
variant="follow"
|
}
|
||||||
content={getUsersActionJsx(followingMembers, 'following the conversation.')}
|
CmdItem.propTypes = {
|
||||||
time=""
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,103 @@
|
||||||
.channel-cmd-bar {
|
.overflow-ellipsis {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmd-bar {
|
||||||
--cmd-bar-height: 28px;
|
--cmd-bar-height: 28px;
|
||||||
min-height: var(--cmd-bar-height);
|
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 {
|
& .timeline-change {
|
||||||
|
width: 100%;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: var(--sp-ultra-tight) var(--sp-normal);
|
padding: var(--sp-ultra-tight) var(--sp-normal);
|
||||||
|
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex: unset;
|
flex: unset;
|
||||||
& > .text {
|
& > .text {
|
||||||
overflow: hidden;
|
@extend .overflow-ellipsis;
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
& b {
|
& b {
|
||||||
color: var(--tc-surface-normal);
|
color: var(--tc-surface-normal);
|
||||||
}
|
}
|
||||||
|
@ -20,3 +105,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ function ChannelViewHeader({ roomId }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
const avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
|
||||||
const roomName = mx.getRoom(roomId).name;
|
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;
|
const roomTopic = mx.getRoom(roomId).currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -46,7 +45,7 @@ function ChannelViewHeader({ roomId }) {
|
||||||
>
|
>
|
||||||
Invite
|
Invite
|
||||||
</MenuItem>
|
</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} />}
|
render={(toggleMenu) => <IconButton onClick={toggleMenu} tooltip="Options" src={VerticalMenuIC} />}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import TextareaAutosize from 'react-autosize-textarea';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
|
import settings from '../../../client/state/settings';
|
||||||
import { bytesToSize } from '../../../util/common';
|
import { bytesToSize } from '../../../util/common';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
@ -22,23 +23,36 @@ import SendIC from '../../../../public/res/ic/outlined/send.svg';
|
||||||
import ShieldIC from '../../../../public/res/ic/outlined/shield.svg';
|
import ShieldIC from '../../../../public/res/ic/outlined/shield.svg';
|
||||||
import VLCIC from '../../../../public/res/ic/outlined/vlc.svg';
|
import VLCIC from '../../../../public/res/ic/outlined/vlc.svg';
|
||||||
import VolumeFullIC from '../../../../public/res/ic/outlined/volume-full.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';
|
import FileIC from '../../../../public/res/ic/outlined/file.svg';
|
||||||
|
|
||||||
|
const CMD_REGEX = /(\/|>[#*@]|:)(\S*)$/;
|
||||||
let isTyping = false;
|
let isTyping = false;
|
||||||
|
let isCmdActivated = false;
|
||||||
|
let cmdCursorPos = null;
|
||||||
function ChannelViewInput({
|
function ChannelViewInput({
|
||||||
roomId, roomTimeline, timelineScroll, viewEvent,
|
roomId, roomTimeline, timelineScroll, viewEvent,
|
||||||
}) {
|
}) {
|
||||||
const [attachment, setAttachment] = useState(null);
|
const [attachment, setAttachment] = useState(null);
|
||||||
|
const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown);
|
||||||
|
|
||||||
const textAreaRef = useRef(null);
|
const textAreaRef = useRef(null);
|
||||||
const inputBaseRef = useRef(null);
|
const inputBaseRef = useRef(null);
|
||||||
const uploadInputRef = useRef(null);
|
const uploadInputRef = useRef(null);
|
||||||
const uploadProgressRef = useRef(null);
|
const uploadProgressRef = useRef(null);
|
||||||
|
const rightOptionsRef = useRef(null);
|
||||||
|
|
||||||
const TYPING_TIMEOUT = 5000;
|
const TYPING_TIMEOUT = 5000;
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const { roomsInput } = initMatrix;
|
const { roomsInput } = initMatrix;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
settings.on(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
|
||||||
|
return () => {
|
||||||
|
settings.removeListener(cons.events.settings.MARKDOWN_TOGGLED, setIsMarkdown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const sendIsTyping = (isT) => {
|
const sendIsTyping = (isT) => {
|
||||||
mx.sendTyping(roomId, isT, isT ? TYPING_TIMEOUT : undefined);
|
mx.sendTyping(roomId, isT, isT ? TYPING_TIMEOUT : undefined);
|
||||||
isTyping = isT;
|
isTyping = isT;
|
||||||
|
@ -63,10 +77,58 @@ function ChannelViewInput({
|
||||||
uploadInputRef.current.value = null;
|
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(() => {
|
useEffect(() => {
|
||||||
roomsInput.on(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
|
roomsInput.on(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
|
||||||
roomsInput.on(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
|
roomsInput.on(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
|
||||||
roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
|
roomsInput.on(cons.events.roomsInput.FILE_UPLOADED, clearAttachment);
|
||||||
|
viewEvent.on('cmd_error', errorCmd);
|
||||||
|
viewEvent.on('cmd_fired', firedCmd);
|
||||||
if (textAreaRef?.current !== null) {
|
if (textAreaRef?.current !== null) {
|
||||||
isTyping = false;
|
isTyping = false;
|
||||||
textAreaRef.current.focus();
|
textAreaRef.current.focus();
|
||||||
|
@ -77,6 +139,9 @@ function ChannelViewInput({
|
||||||
roomsInput.removeListener(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
|
roomsInput.removeListener(cons.events.roomsInput.UPLOAD_PROGRESS_CHANGES, uploadingProgress);
|
||||||
roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
|
roomsInput.removeListener(cons.events.roomsInput.ATTACHMENT_CANCELED, clearAttachment);
|
||||||
roomsInput.removeListener(cons.events.roomsInput.FILE_UPLOADED, 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;
|
if (textAreaRef?.current === null) return;
|
||||||
|
|
||||||
const msg = textAreaRef.current.value;
|
const msg = textAreaRef.current.value;
|
||||||
|
@ -90,6 +155,11 @@ function ChannelViewInput({
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
|
if (isCmdActivated) {
|
||||||
|
viewEvent.emit('cmd_exe');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const msgBody = textAreaRef.current.value;
|
const msgBody = textAreaRef.current.value;
|
||||||
if (roomsInput.isSending(roomId)) return;
|
if (roomsInput.isSending(roomId)) return;
|
||||||
if (msgBody.trim() === '' && attachment === null) return;
|
if (msgBody.trim() === '' && attachment === null) return;
|
||||||
|
@ -124,9 +194,39 @@ function ChannelViewInput({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function handleMsgTyping(e) {
|
||||||
const msg = e.target.value;
|
const msg = e.target.value;
|
||||||
processTyping(msg);
|
recognizeCmd(e.target.value);
|
||||||
|
if (!isCmdActivated) processTyping(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(e) {
|
function handleKeyDown(e) {
|
||||||
|
@ -172,8 +272,9 @@ function ChannelViewInput({
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
{isMarkdown && <RawIcon size="extra-small" src={MarkdownIC} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="channel-input__option-container">
|
<div ref={rightOptionsRef} className="channel-input__option-container">
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
placement="top"
|
placement="top"
|
||||||
content={(
|
content={(
|
||||||
|
|
|
@ -23,14 +23,19 @@
|
||||||
|
|
||||||
& > .ic-raw {
|
& > .ic-raw {
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
margin-left: var(--sp-extra-tight);
|
margin: 0 var(--sp-extra-tight);
|
||||||
[dir=rtl] & {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: var(--sp-extra-tight);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
& .scrollbar {
|
& .scrollbar {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: var(--sp-tight);
|
||||||
|
[dir=rtl] & {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: var(--sp-tight);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +49,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding: var(--sp-ultra-tight) calc(var(--sp-tight) - 2px);
|
padding: var(--sp-ultra-tight) 0;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--tc-surface-low);
|
color: var(--tc-surface-low);
|
||||||
|
|
|
@ -88,7 +88,7 @@ function SearchedEmoji() {
|
||||||
setSearchedEmojis([]);
|
setSearchedEmojis([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSearchedEmojis(searchEmoji(term));
|
setSearchedEmojis(searchEmoji(term).map((finding) => finding.item));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -68,7 +68,7 @@ function searchEmoji(term) {
|
||||||
|
|
||||||
let result = fuse.search(term);
|
let result = fuse.search(term);
|
||||||
if (result.length > 20) result = result.slice(0, 20);
|
if (result.length > 20) result = result.slice(0, 20);
|
||||||
return result.map((finding) => finding.item);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
12
src/client/action/settings.js
Normal file
12
src/client/action/settings.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import appDispatcher from '../dispatcher';
|
||||||
|
import cons from '../state/cons';
|
||||||
|
|
||||||
|
function toggleMarkdown() {
|
||||||
|
appDispatcher.dispatch({
|
||||||
|
type: cons.actions.settings.TOGGLE_MARKDOWN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
toggleMarkdown,
|
||||||
|
};
|
|
@ -22,9 +22,12 @@ const cons = {
|
||||||
LEAVE: 'LEAVE',
|
LEAVE: 'LEAVE',
|
||||||
CREATE: 'CREATE',
|
CREATE: 'CREATE',
|
||||||
error: {
|
error: {
|
||||||
CREATE: 'CREATE',
|
CREATE: 'ERROR_CREATE',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
TOGGLE_MARKDOWN: 'TOGGLE_MARKDOWN',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
navigation: {
|
navigation: {
|
||||||
|
@ -57,6 +60,9 @@ const cons = {
|
||||||
FILE_UPLOAD_CANCELED: 'FILE_UPLOAD_CANCELED',
|
FILE_UPLOAD_CANCELED: 'FILE_UPLOAD_CANCELED',
|
||||||
ATTACHMENT_CANCELED: 'ATTACHMENT_CANCELED',
|
ATTACHMENT_CANCELED: 'ATTACHMENT_CANCELED',
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
MARKDOWN_TOGGLED: 'MARKDOWN_TOGGLED',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,36 @@
|
||||||
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() {
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
this.themes = ['', 'silver-theme', 'dark-theme', 'butter-theme'];
|
this.themes = ['', 'silver-theme', 'dark-theme', 'butter-theme'];
|
||||||
this.themeIndex = this.getThemeIndex();
|
this.themeIndex = this.getThemeIndex();
|
||||||
|
|
||||||
|
this.isMarkdown = this.getIsMarkdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
getThemeIndex() {
|
getThemeIndex() {
|
||||||
if (typeof this.themeIndex === 'number') return this.themeIndex;
|
if (typeof this.themeIndex === 'number') return this.themeIndex;
|
||||||
|
|
||||||
let settings = localStorage.getItem('settings');
|
const settings = getSettings();
|
||||||
if (settings === null) return 0;
|
if (settings === null) return 0;
|
||||||
settings = JSON.parse(settings);
|
|
||||||
if (typeof settings.themeIndex === 'undefined') return 0;
|
if (typeof settings.themeIndex === 'undefined') return 0;
|
||||||
// eslint-disable-next-line radix
|
// eslint-disable-next-line radix
|
||||||
return parseInt(settings.themeIndex);
|
return parseInt(settings.themeIndex);
|
||||||
|
@ -26,11 +47,33 @@ class Settings {
|
||||||
appBody.classList.remove(themeName);
|
appBody.classList.remove(themeName);
|
||||||
});
|
});
|
||||||
if (this.themes[themeIndex] !== '') appBody.classList.add(this.themes[themeIndex]);
|
if (this.themes[themeIndex] !== '') appBody.classList.add(this.themes[themeIndex]);
|
||||||
localStorage.setItem('settings', JSON.stringify({ themeIndex }));
|
setSettings('themeIndex', themeIndex);
|
||||||
this.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();
|
const settings = new Settings();
|
||||||
|
appDispatcher.register(settings.setter.bind(settings));
|
||||||
|
|
||||||
export default settings;
|
export default settings;
|
||||||
|
|
Loading…
Reference in a new issue