mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 14:30:29 +03:00 
			
		
		
		
	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,22 +1,134 @@
 | 
				
			||||||
.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);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.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…
	
	Add table
		Add a link
		
	
		Reference in a new issue