Merge pull request #2 from status-im/emoji

Add emoji support
This commit is contained in:
Iuri Matias 2018-11-23 17:03:29 -05:00 committed by GitHub
commit a18f70fdb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 187 additions and 115 deletions

View File

@ -5,6 +5,7 @@
<title>Status</title> <title>Status</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="https://unpkg.com/emoji-mart@2.8.1/css/emoji-mart.css" />
<script> <script>
(function() { (function() {
if (!process.env.HOT) { if (!process.env.HOT) {

View File

@ -7,6 +7,7 @@ import Avatar from '@material-ui/core/Avatar';
import YouTube from 'react-youtube'; import YouTube from 'react-youtube';
import Linkify from 'react-linkify'; import Linkify from 'react-linkify';
import SpotifyPlayer from 'react-spotify-player'; import SpotifyPlayer from 'react-spotify-player';
import { Emoji } from 'emoji-mart';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon' import Jazzicon, { jsNumberForAddress } from 'react-jazzicon'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { atomDark } from 'react-syntax-highlighter/dist/styles/prism'; import { atomDark } from 'react-syntax-highlighter/dist/styles/prism';
@ -40,12 +41,25 @@ function getYoutubeId(url) {
return ID; return ID;
} }
//TODO use regex for code parsing / detection. Add new line detection for shift+enter // TODO use regex for code parsing / detection. Add new line detection for shift+enter
const MessageRender = ({ message }) => ( const MessageRender = ({ message }) => {
message[2] === "`" && SyntaxLookup[message.slice(0,2)] const emojis = [];
? <SyntaxHighlighter language={SyntaxLookup[message.slice(0,2)]} style={atomDark}>{message.slice(3)}</SyntaxHighlighter> let match;
: <Linkify><span style={{ wordWrap: 'break-word', whiteSpace: 'pre-line' }}>{message}</span></Linkify> const regex1 = RegExp(/:[\-a-zA-Z_]+:/g);
); while ((match = regex1.exec(message)) !== null) {
emojis.push(<Emoji emoji={match[0]} size={16} />);
}
const parts = message.split(regex1);
parts.forEach((part, i) => {
parts[i] = <span className="match" key={i}>{part}{emojis[i]}</span>;
});
return (message[2] === "`" && SyntaxLookup[message.slice(0,2)]
? <SyntaxHighlighter language={SyntaxLookup[message.slice(0,2)]} style={atomDark}>{message.slice(3)}</SyntaxHighlighter>
: <Linkify><span style={{ wordWrap: 'break-word', whiteSpace: 'pre-line' }}>{parts}</span></Linkify>)
};
class ChatBox extends PureComponent { class ChatBox extends PureComponent {
state = { state = {
@ -64,7 +78,6 @@ class ChatBox extends PureComponent {
const arrayBufferView = new Uint8Array(content); const arrayBufferView = new Uint8Array(content);
const blob = new Blob([ arrayBufferView ], { type: "image/jpeg" }); const blob = new Blob([ arrayBufferView ], { type: "image/jpeg" });
const imgUrl = URL.createObjectURL(blob); const imgUrl = URL.createObjectURL(blob);
const image = `data:image/png;base64,${content.toString('base64')}`;
this.setState({ imgUrl }); this.setState({ imgUrl });
}; };
@ -77,33 +90,33 @@ class ChatBox extends PureComponent {
<Avatar> <Avatar>
<ListItemAvatar> <ListItemAvatar>
<Avatar> <Avatar>
{pubkey && <Jazzicon diameter={40} seed={jsNumberForAddress(pubkey)} />} {pubkey && <Jazzicon diameter={40} seed={jsNumberForAddress(pubkey)}/>}
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
</Avatar> </Avatar>
<ListItemText primary={`${username}`} secondary={<MessageRender message={message} />} /> <ListItemText primary={`${username}`} secondary={<MessageRender message={message}/>}/>
</ListItem> </ListItem>
{hasYoutubeLink(message) && {hasYoutubeLink(message) &&
<ListItem> <ListItem>
<YouTube <YouTube
videoId={getYoutubeId(message)} videoId={getYoutubeId(message)}
opts={{height: '390', width: '640', playerVars: { autoplay: 0 }}} opts={{ height: '390', width: '640', playerVars: { autoplay: 0 } }}
/> />
</ListItem> </ListItem>
} }
{isSpotifyLink(message) && {isSpotifyLink(message) &&
<ListItem> <ListItem>
<SpotifyPlayer <SpotifyPlayer
uri={message} uri={message}
size={{'width': 300, 'height': 300}} size={{ 'width': 300, 'height': 300 }}
view='list' view='list'
theme='black' theme='black'
/> />
</ListItem> </ListItem>
} }
{!!imgUrl && <img src={imgUrl} alt='ipfs' style={{ width: '100%' }}/>} {!!imgUrl && <img src={imgUrl} alt='ipfs' style={{ width: '100%' }}/>}
</Fragment> </Fragment>
) );
}; };
} }

View File

@ -1,5 +1,5 @@
// @flow // @flow
import React, { Fragment, PureComponent, createRef } from 'react'; import React, { Fragment, Component, PureComponent, createRef } from 'react';
import { Formik } from 'formik'; import { Formik } from 'formik';
import autoscroll from 'autoscroll-react'; import autoscroll from 'autoscroll-react';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
@ -10,13 +10,19 @@ import ListItemText from '@material-ui/core/ListItemText';
import Divider from '@material-ui/core/Divider'; import Divider from '@material-ui/core/Divider';
import Grid from '@material-ui/core/Grid'; import Grid from '@material-ui/core/Grid';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import Dropzone from 'react-dropzone'; import Dropzone from 'react-dropzone';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon'; import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
import { Picker } from 'emoji-mart';
import ChatBox from './ChatBox'; import ChatBox, { Emoji } from './ChatBox';
import ChatHeader from './ChatHeader'; import ChatHeader from './ChatHeader';
import { uploadFileAndSend } from '../utils/ipfs'; import { uploadFileAndSend } from '../utils/ipfs';
import 'emoji-mart/css/emoji-mart.css';
class WhoIsTyping extends PureComponent { class WhoIsTyping extends PureComponent {
whoIsTyping() { whoIsTyping() {
@ -68,96 +74,138 @@ const AutoScrollList = autoscroll(List);
const formStyle = { display: 'flex', justifyContent: 'center', alignItems: 'center', flexBasis: '10%' }; const formStyle = { display: 'flex', justifyContent: 'center', alignItems: 'center', flexBasis: '10%' };
const listStyle = { overflowY: 'auto', flexBasis: '76%', position: 'absolute', top: '72px', left: 0, right: 0, bottom: '67px' }; const listStyle = { overflowY: 'auto', flexBasis: '76%', position: 'absolute', top: '72px', left: 0, right: 0, bottom: '67px' };
const ChatRoomForm = createRef(); const ChatRoomForm = createRef();
const ChatRoom = ({ messages, sendMessage, currentChannel, usersTyping, typingEvent, channelUsers, allUsers, ipfs }) => ( class ChatRoom extends Component {
<Grid container style={{ height: '100vh'}}> constructor(props) {
<Grid xs={8} item > super(props);
<Dropzone this.state = {
onDrop={(a, r) => { onDrop(a,r,ipfs,sendMessage) } } showEmojis: false
disableClick };
style={{ position: 'relative', height: '100%' }} }
activeStyle={{ backgroundColor: 'grey', outline: '5px dashed lightgrey', alignSelf: 'center', outlineOffset: '-10px' }}>
<Grid toggleEmojis(e) {
container e.preventDefault();
direction="column" this.setState(({showEmojis: !this.state.showEmojis}));
justify="flex-start" }
alignItems="stretch"
style={{ height: '100%' }} addEmoji(emoji, chatInput, setValue) {
> console.log(emoji);
<ChatHeader currentChannel={currentChannel}/> setValue('chatInput', `${chatInput}:${emoji.id}:`);
<Divider /> this.setState(({showEmojis: false}));
<AutoScrollList style={listStyle}> // <Emoji emoji=":santa::skin-tone-3:" size={16} />
{messages[currentChannel] && messages[currentChannel].map((message) => ( }
<Fragment key={message.data.payload}>
<ChatBox {...message} ipfs={ipfs} /> render() {
<li> const { messages, sendMessage, currentChannel, usersTyping, typingEvent, channelUsers, allUsers, ipfs } = this.props;
<Divider /> const {showEmojis} = this.state;
</li> return (
</Fragment> <Grid container style={{ height: '100vh' }}>
))} <Grid xs={8} item>
</AutoScrollList> <Dropzone
<Formik onDrop={(a, r) => {
initialValues={{ chatInput: '' }} onDrop(a, r, ipfs, sendMessage);
onSubmit={(values, { setSubmitting, resetForm }) => {
const { chatInput } = values;
sendMessage(chatInput);
resetForm();
setSubmitting(false);
}} }}
> disableClick
{({ style={{ position: 'relative', height: '100%' }}
values, activeStyle={{
errors, backgroundColor: 'grey',
touched, outline: '5px dashed lightgrey',
handleChange, alignSelf: 'center',
handleBlur, outlineOffset: '-10px'
handleSubmit, }}>
setFieldValue <Grid
}) => ( container
<div className="chat-input" style={{position: 'absolute', bottom: 0, left: 0, right: 0, paddingBottom: 10}}> direction="column"
<form onSubmit={handleSubmit} style={formStyle} ref={ChatRoomForm}> justify="flex-start"
<TextField alignItems="stretch"
id="chatInput" style={{ height: '100%' }}
multiline >
style={{ width: 'auto', flexGrow: '0.95', margin: '2px 0 0 0' }} <ChatHeader currentChannel={currentChannel}/>
label="Type a message..." <Divider/>
type="text" <AutoScrollList style={listStyle}>
name="chatInput" {messages[currentChannel] && messages[currentChannel].map((message) => (
margin="normal" <Fragment key={message.data.payload}>
variant="outlined" <ChatBox {...message} ipfs={ipfs}/>
fullWidth <li>
onChange={handleChange} <Divider/>
onKeyDown={(e) => keyDownHandler(e, typingEvent, setFieldValue, values.chatInput)} </li>
onBlur={handleBlur} </Fragment>
value={values.chatInput || ''} ))}
/> </AutoScrollList>
{errors.chatInput && touched.chatInput && errors.chatInput} <Formik
</form> initialValues={{ chatInput: '' }}
<WhoIsTyping onSubmit={(values, { setSubmitting, resetForm }) => {
currentChannel={currentChannel} const { chatInput } = values;
usersTyping={usersTyping} sendMessage(chatInput);
users={allUsers} /> resetForm();
</div> setSubmitting(false);
)} }}
</Formik> >
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
setFieldValue
}) => (
<div className="chat-input"
style={{ position: 'absolute', bottom: 0, left: 0, right: 0, paddingBottom: 10 }}>
<form onSubmit={handleSubmit} style={formStyle} ref={ChatRoomForm}>
<TextField
id="chatInput"
multiline
style={{ width: 'auto', flexGrow: '0.95', margin: '2px 0 0 0' }}
label="Type a message..."
type="text"
name="chatInput"
margin="normal"
variant="outlined"
fullWidth
onChange={handleChange}
onKeyDown={(e) => keyDownHandler(e, typingEvent, setFieldValue, values.chatInput)}
onBlur={handleBlur}
value={values.chatInput || ''}
/>
{showEmojis && <Picker onSelect={(emoji) => this.addEmoji(emoji, values.chatInput, setFieldValue)}
style={{ position: 'absolute', bottom: '80px', right: '20px' }}/>}
<Button onClick={(e) => this.toggleEmojis(e)}>Smile</Button>
{errors.chatInput && touched.chatInput && errors.chatInput}
</form>
<WhoIsTyping
currentChannel={currentChannel}
usersTyping={usersTyping}
users={allUsers}/>
</div>
)}
</Formik>
</Grid>
</Dropzone>
</Grid> </Grid>
</Dropzone> <Grid xs={4} item style={{ overflow: 'auto' }}>
</Grid> <List>
<Grid xs={4} item style={{overflow: 'auto'}}> {Object.keys(channelUsers).map(user => (
<List> <ListItem button key={user}>
{Object.keys(channelUsers).map(user => ( <span className="dot" style={{
<ListItem button key={user}> 'height': '10px',
<span className="dot" style={{"height": "10px", "width": "11px", "background-color": (allUsers[user].online ? "lightgreen" : "lightgrey"), "border-radius": "50%", "margin-right": "10px"}}/> 'width': '11px',
<ListItemAvatar> 'background-color': (allUsers[user].online ? 'lightgreen' : 'lightgrey'),
<Avatar> 'border-radius': '50%',
<Jazzicon diameter={40} seed={jsNumberForAddress(user)} /> 'margin-right': '10px'
</Avatar> }}/>
</ListItemAvatar> <ListItemAvatar>
<ListItemText primary={allUsers[user].username} /> <Avatar>
</ListItem> <Jazzicon diameter={40} seed={jsNumberForAddress(user)}/>
))} </Avatar>
</List> </ListItemAvatar>
</Grid> <ListItemText primary={allUsers[user].username}/>
</Grid> </ListItem>
); ))}
</List>
</Grid>
</Grid>
)
}
}
export default ChatRoom; export default ChatRoom;

5
package-lock.json generated
View File

@ -7094,6 +7094,11 @@
"minimalistic-crypto-utils": "^1.0.0" "minimalistic-crypto-utils": "^1.0.0"
} }
}, },
"emoji-mart": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-2.8.1.tgz",
"integrity": "sha512-9ScBU9ObG4qVQOMvQWQwUkYVNK/eOu/TRk8o+LABtnL2BE/o9CU4OvtjcoBPNiC/7QtRhW9vOwmvUx/OjT+mwA=="
},
"emoji-regex": { "emoji-regex": {
"version": "6.5.1", "version": "6.5.1",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",

View File

@ -264,6 +264,7 @@
"electron-debug": "^2.0.0", "electron-debug": "^2.0.0",
"electron-log": "^2.2.17", "electron-log": "^2.2.17",
"electron-updater": "^3.1.6", "electron-updater": "^3.1.6",
"emoji-mart": "^2.8.1",
"eth-keyring-controller": "^3.3.1", "eth-keyring-controller": "^3.3.1",
"formik": "^1.3.1", "formik": "^1.3.1",
"history": "^4.7.2", "history": "^4.7.2",

View File

@ -5144,6 +5144,10 @@ elliptic@^6.0.0, elliptic@^6.2.3, elliptic@^6.4.0:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0" minimalistic-crypto-utils "^1.0.0"
emoji-mart@^2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-2.8.1.tgz#4ffcb807237989a953085c80a6553d21f1effc62"
emoji-regex@^6.5.1: emoji-regex@^6.5.1:
version "6.5.1" version "6.5.1"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"