diff --git a/public/res/ic/outlined/cmd.svg b/public/res/ic/outlined/cmd.svg new file mode 100644 index 0000000..75ae0d9 --- /dev/null +++ b/public/res/ic/outlined/cmd.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/markdown.svg b/public/res/ic/outlined/markdown.svg new file mode 100644 index 0000000..775afbf --- /dev/null +++ b/public/res/ic/outlined/markdown.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/app/organisms/channel/ChannelViewCmdBar.jsx b/src/app/organisms/channel/ChannelViewCmdBar.jsx index b7006c8..6229b37 100644 --- a/src/app/organisms/channel/ChannelViewCmdBar.jsx +++ b/src/app/organisms/channel/ChannelViewCmdBar.jsx @@ -2,15 +2,119 @@ 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 ( + + General command + /command_name + Go-to commands + {'>*space_name'} + {'>#channel_name'} + {'>@people_name'} + Autofill command + :emoji_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; @@ -26,9 +130,7 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) { setFollowingMembers(userIds.filter((userId) => userId !== myUserId)); } - useEffect(() => { - updateFollowingMembers(); - }, [roomId]); + useEffect(() => updateFollowingMembers(), [roomId]); useEffect(() => { roomTimeline.on(cons.events.roomTimeline.READ_RECEIPT, updateFollowingMembers); @@ -39,17 +141,264 @@ function ChannelViewCmdBar({ roomId, roomTimeline, viewEvent }) { }; }, [roomTimeline]); + return followingMembers.length !== 0 && ( + + ); +} + +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 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 ( -
- { - followingMembers.length !== 0 && ( - - ) + + ); +} +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) => ( + { + fireCmd({ + prefix: cmdPrefix, + slug: roomSlug, + result: finding.item, + }); + }} + > + {finding.item.name} + + )); + } + + 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 ( + { + fireCmd({ + prefix: cmdPrefix, + slug: cmdSlug, + option: cmdSlugOption, + result: finding.item, + }); + }} + > + {`${finding.item.name}${option}`} + + ); + }); + } + + 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) => ( + fireCmd({ + prefix: emPrefix, + slug: shortcutSlug, + result: finding.item, + })} + > + {finding.item.unicode} + + )); + } + + 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 ( +
+
+
+
+
+ {cmd.error} +
+
+ ); + } + + return ( +
+
+ {cmd === null && } + {cmd !== null && typeof cmd.slug === 'undefined' &&
} + {cmd !== null && typeof cmd.slug === 'string' && TAB} +
+
+ {cmd === null && ( + + )} + {cmd !== null && typeof cmd.slug === 'undefined' && {getCmdActivationMessage(cmd.prefix)}} + {cmd !== null && typeof cmd.slug === 'string' && ( + +
{getCmdSuggestions(cmd, fireCmd, viewEvent)}
+
+ )} +
+
+ {cmd !== null && cmd.prefix === '/' && } +
); } diff --git a/src/app/organisms/channel/ChannelViewCmdBar.scss b/src/app/organisms/channel/ChannelViewCmdBar.scss index 7c14f74..43450fe 100644 --- a/src/app/organisms/channel/ChannelViewCmdBar.scss +++ b/src/app/organisms/channel/ChannelViewCmdBar.scss @@ -1,22 +1,134 @@ -.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 diff --git a/src/app/organisms/channel/ChannelViewHeader.jsx b/src/app/organisms/channel/ChannelViewHeader.jsx index a9d4551..b9f56d8 100644 --- a/src/app/organisms/channel/ChannelViewHeader.jsx +++ b/src/app/organisms/channel/ChannelViewHeader.jsx @@ -21,7 +21,6 @@ function ChannelViewHeader({ roomId }) { 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 ( @@ -46,7 +45,7 @@ function ChannelViewHeader({ roomId }) { > Invite - roomActions.leave(roomId, isDM)}>Leave + roomActions.leave(roomId)}>Leave )} render={(toggleMenu) => } diff --git a/src/app/organisms/channel/ChannelViewInput.jsx b/src/app/organisms/channel/ChannelViewInput.jsx index 67b002f..0a40117 100644 --- a/src/app/organisms/channel/ChannelViewInput.jsx +++ b/src/app/organisms/channel/ChannelViewInput.jsx @@ -7,6 +7,7 @@ import TextareaAutosize from 'react-autosize-textarea'; 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'; @@ -22,23 +23,36 @@ import SendIC from '../../../../public/res/ic/outlined/send.svg'; 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; @@ -63,10 +77,58 @@ function ChannelViewInput({ 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(); @@ -77,6 +139,9 @@ function ChannelViewInput({ 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; @@ -90,6 +155,11 @@ function ChannelViewInput({ }, [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; @@ -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) { const msg = e.target.value; - processTyping(msg); + recognizeCmd(e.target.value); + if (!isCmdActivated) processTyping(msg); } function handleKeyDown(e) { @@ -172,8 +272,9 @@ function ChannelViewInput({ /> + {isMarkdown && }
-
+
.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); + } + } } } @@ -44,7 +49,7 @@ 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); diff --git a/src/app/organisms/emoji-board/EmojiBoard.jsx b/src/app/organisms/emoji-board/EmojiBoard.jsx index e4c2e75..5428eaa 100644 --- a/src/app/organisms/emoji-board/EmojiBoard.jsx +++ b/src/app/organisms/emoji-board/EmojiBoard.jsx @@ -88,7 +88,7 @@ function SearchedEmoji() { setSearchedEmojis([]); return; } - setSearchedEmojis(searchEmoji(term)); + setSearchedEmojis(searchEmoji(term).map((finding) => finding.item)); } useEffect(() => { diff --git a/src/app/organisms/emoji-board/emoji.js b/src/app/organisms/emoji-board/emoji.js index e759d59..821b0a2 100644 --- a/src/app/organisms/emoji-board/emoji.js +++ b/src/app/organisms/emoji-board/emoji.js @@ -68,7 +68,7 @@ function searchEmoji(term) { let result = fuse.search(term); if (result.length > 20) result = result.slice(0, 20); - return result.map((finding) => finding.item); + return result; } export { diff --git a/src/client/action/settings.js b/src/client/action/settings.js new file mode 100644 index 0000000..1664eb8 --- /dev/null +++ b/src/client/action/settings.js @@ -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, +}; diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 9ecd1df..9b30031 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -22,9 +22,12 @@ const cons = { LEAVE: 'LEAVE', CREATE: 'CREATE', error: { - CREATE: 'CREATE', + CREATE: 'ERROR_CREATE', }, }, + settings: { + TOGGLE_MARKDOWN: 'TOGGLE_MARKDOWN', + }, }, events: { navigation: { @@ -57,6 +60,9 @@ const cons = { FILE_UPLOAD_CANCELED: 'FILE_UPLOAD_CANCELED', ATTACHMENT_CANCELED: 'ATTACHMENT_CANCELED', }, + settings: { + MARKDOWN_TOGGLED: 'MARKDOWN_TOGGLED', + }, }, }; diff --git a/src/client/state/settings.js b/src/client/state/settings.js index 1b9dfc2..fcbf4be 100644 --- a/src/client/state/settings.js +++ b/src/client/state/settings.js @@ -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() { + 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); @@ -26,11 +47,33 @@ class Settings { 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;