Refactor Chat

This commit is contained in:
Jeremie Pardou-Piquemal 2022-12-15 23:15:55 +01:00
parent 7eb31f13e3
commit 31db176328

View File

@ -1,171 +1,134 @@
import React, { Component } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import { CornerDownRight } from 'react-feather'; import { CornerDownRight } from 'react-feather';
import { getSelectedText, hasTouchSupport } from '@/utils/dom'; import { hasTouchSupport } from '@/utils/dom';
import FileTransfer from '@/components/FileTransfer'; import FileTransfer from '@/components/FileTransfer';
export class Chat extends Component { export const Chat = ({ sendEncryptedMessage, showNotice, userId, username, clearActivities, translations }) => {
constructor(props) { const [message, setMessage] = React.useState('');
super(props); const [shiftKeyDown, setShiftKeyDown] = React.useState(false);
this.state = { const textInputRef = React.useRef();
message: '',
touchSupport: hasTouchSupport,
shiftKeyDown: false,
};
this.commands = [ const touchSupport = hasTouchSupport;
{
command: 'nick',
description: 'Changes nickname.',
parameters: ['{username}'],
usage: '/nick {username}',
scope: 'global',
action: params => {
// eslint-disable-line
let newUsername = params.join(' ') || ''; // eslint-disable-line
// Remove things that aren't digits or chars const canSend = message.trim().length;
newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
const errors = []; const commands = [
{
command: 'nick',
description: 'Changes nickname.',
parameters: ['{username}'],
usage: '/nick {username}',
scope: 'global',
action: params => {
// eslint-disable-line
let newUsername = params.join(' ') || ''; // eslint-disable-line
if (!newUsername.trim().length) { // Remove things that aren't digits or chars
errors.push('Username cannot be blank'); newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
}
if (newUsername.toString().length > 16) { const errors = [];
errors.push('Username cannot be greater than 16 characters');
}
if (!newUsername.match(/^[A-Z]/i)) { if (!newUsername.trim().length) {
errors.push('Username must start with a letter'); errors.push('Username cannot be blank');
} }
if (errors.length) { if (newUsername.toString().length > 16) {
return this.props.showNotice({ errors.push('Username cannot be greater than 16 characters');
message: `${errors.join(', ')}`, }
level: 'error',
});
}
this.props.sendEncryptedMessage({ if (!newUsername.match(/^[A-Z]/i)) {
type: 'CHANGE_USERNAME', errors.push('Username must start with a letter');
payload: { }
id: this.props.userId,
newUsername, if (errors.length) {
currentUsername: this.props.username, return showNotice({
}, message: `${errors.join(', ')}`,
level: 'error',
}); });
}, }
},
{
command: 'help',
description: 'Shows a list of commands.',
paramaters: [],
usage: '/help',
scope: 'local',
action: params => {
// eslint-disable-line
const validCommands = this.commands.map(command => `/${command.command}`);
this.props.showNotice({
message: `Valid commands: ${validCommands.sort().join(', ')}`,
level: 'info',
});
},
},
{
command: 'me',
description: 'Invoke virtual action',
paramaters: ['{action}'],
usage: '/me {action}',
scope: 'global',
action: params => {
// eslint-disable-line
const actionMessage = params.join(' ');
if (!actionMessage.trim().length) {
return false;
}
this.props.sendEncryptedMessage({ sendEncryptedMessage({
type: 'USER_ACTION', type: 'CHANGE_USERNAME',
payload: { payload: {
action: actionMessage, id: userId,
}, newUsername,
}); currentUsername: username,
}, },
},
{
command: 'clear',
description: 'Clears the chat screen',
paramaters: [],
usage: '/clear',
scope: 'local',
action: (params = null) => {
// eslint-disable-line
this.props.clearActivities();
},
},
];
}
componentDidMount() {
if (!hasTouchSupport) {
// Disable for now due to vary issues:
// Paste not working, shift+enter line breaks
// autosize(this.textInput);
this.textInput.addEventListener('autosize:resized', () => {
this.props.scrollToBottom();
});
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.focusChat) {
if (!getSelectedText()) {
// Don't focus for now, evaluate UX benfits
// this.textInput.focus()
}
}
}
componentDidUpdate(nextProps, nextState) {
if (!nextState.message.trim().length) {
// autosize.update(this.textInput)
}
}
handleKeyUp(e) {
if (e.key === 'Shift') {
this.setState({
shiftKeyDown: false,
});
}
}
handleKeyPress(e) {
if (e.key === 'Shift') {
this.setState({
shiftKeyDown: true,
});
}
// Fix when autosize is enabled - line breaks require shift+enter twice
if (e.key === 'Enter' && !hasTouchSupport && !this.state.shiftKeyDown) {
e.preventDefault();
if (this.canSend()) {
this.sendMessage();
} else {
this.setState({
message: '',
}); });
},
},
{
command: 'help',
description: 'Shows a list of commands.',
parameters: [],
usage: '/help',
scope: 'local',
action: () => {
const validCommands = commands.map(command => `/${command.command}`);
showNotice({
message: `Valid commands: ${validCommands.sort().join(', ')}`,
level: 'info',
});
},
},
{
command: 'me',
description: 'Invoke virtual action',
parameters: ['{action}'],
usage: '/me {action}',
scope: 'global',
action: params => {
const actionMessage = params.join(' ');
if (!actionMessage.trim().length) {
return false;
}
sendEncryptedMessage({
type: 'USER_ACTION',
payload: {
action: actionMessage,
},
});
},
},
{
command: 'clear',
description: 'Clears the chat screen',
parameters: [],
usage: '/clear',
scope: 'local',
action: () => {
clearActivities();
},
},
];
const handleKeyUp = e => {
if (e.key === 'Shift') {
setShiftKeyDown(false);
}
};
const handleKeyPress = e => {
if (e.key === 'Shift') {
setShiftKeyDown(true);
}
if (e.key === 'Enter' && !hasTouchSupport && !shiftKeyDown) {
e.preventDefault();
if (canSend) {
sendMessage();
} else {
setMessage('');
} }
} }
} };
executeCommand(command) { const executeCommand = command => {
const commandToExecute = this.commands.find(cmnd => cmnd.command === command.command); const commandToExecute = commands.find(cmnd => cmnd.command === command.command);
if (commandToExecute) { if (commandToExecute) {
const { params } = command; const { params } = command;
@ -175,19 +138,19 @@ export class Chat extends Component {
} }
return null; return null;
} };
handleSendClick() { const handleSendClick = () => {
this.sendMessage.bind(this); sendMessage();
this.textInput.focus(); textInputRef.current.focus();
} };
handleFormSubmit(evt) { const handleFormSubmit = evt => {
evt.preventDefault(); evt.preventDefault();
this.sendMessage(); sendMessage();
} };
parseCommand(message) { const parseCommand = message => {
const commandTrigger = { const commandTrigger = {
command: null, command: null,
params: [], params: [],
@ -207,23 +170,22 @@ export class Chat extends Component {
} }
return false; return false;
} };
sendMessage() { const sendMessage = () => {
if (!this.canSend()) { if (!canSend) {
return; return;
} }
const { message } = this.state; const isCommand = parseCommand(message);
const isCommand = this.parseCommand(message);
if (isCommand) { if (isCommand) {
const res = this.executeCommand(isCommand); const res = executeCommand(isCommand);
if (res === false) { if (res === false) {
return; return;
} }
} else { } else {
this.props.sendEncryptedMessage({ sendEncryptedMessage({
type: 'TEXT_MESSAGE', type: 'TEXT_MESSAGE',
payload: { payload: {
text: message, text: message,
@ -232,55 +194,41 @@ export class Chat extends Component {
}); });
} }
this.setState({ setMessage('');
message: '', };
});
}
handleInputChange(evt) { const handleInputChange = evt => {
this.setState({ setMessage(evt.target.value);
message: evt.target.value, };
});
}
canSend() { return (
return this.state.message.trim().length; <form onSubmit={handleFormSubmit} className="chat-preflight-container">
} <textarea
rows="1"
render() { onKeyUp={handleKeyUp}
const touchSupport = this.state.touchSupport; onKeyDown={handleKeyPress}
ref={textInputRef}
return ( autoFocus
<form onSubmit={this.handleFormSubmit.bind(this)} className="chat-preflight-container"> className="chat"
<textarea value={message}
rows="1" placeholder={translations.typePlaceholder}
onKeyUp={this.handleKeyUp.bind(this)} onChange={handleInputChange}
onKeyDown={this.handleKeyPress.bind(this)} />
ref={input => { <div className="input-controls">
this.textInput = input; <FileTransfer sendEncryptedMessage={sendEncryptedMessage} />
}} {touchSupport && (
autoFocus <button
className="chat" onClick={handleSendClick}
value={this.state.message} className={`icon is-right send btn btn-link ${canSend ? 'active' : ''}`}
placeholder={this.props.translations.typePlaceholder} title="Send"
onChange={this.handleInputChange.bind(this)} >
/> <CornerDownRight className={canSend ? '' : 'disabled'} />
<div className="input-controls"> </button>
<FileTransfer sendEncryptedMessage={this.props.sendEncryptedMessage} /> )}
{touchSupport && ( </div>
<button </form>
onClick={this.handleSendClick.bind(this)} );
className={`icon is-right send btn btn-link ${this.canSend() ? 'active' : ''}`} };
title="Send"
>
<CornerDownRight className={this.canSend() ? '' : 'disabled'} />
</button>
)}
</div>
</form>
);
}
}
Chat.propTypes = { Chat.propTypes = {
sendEncryptedMessage: PropTypes.func.isRequired, sendEncryptedMessage: PropTypes.func.isRequired,