diff --git a/package.json b/package.json index 2e5fce6..a7c1e9f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "homepage": "https://pusher.github.io/react-slack-clone", "dependencies": { "@pusher/chatkit": "^0.7.9", + "emoji-js": "^3.4.0", + "emoji-picker-react": "^2.0.4", "object-path-immutable": "^1.0.1", "react": "^16.2.0", "react-dom": "^16.2.0", diff --git a/src/components/CreateMessageForm/index.js b/src/components/CreateMessageForm/index.js index 0f1685c..e7d90ed 100644 --- a/src/components/CreateMessageForm/index.js +++ b/src/components/CreateMessageForm/index.js @@ -1,36 +1,109 @@ -import React from 'react' +import React, { Component } from 'react' import style from './index.module.css' import { FileInput } from '../FileInput' +import EmojiPicker from '../EmojiPicker' -export const CreateMessageForm = ({ - state: { user = {}, room = {}, message = '' }, - actions: { runCommand }, -}) => - room.id ? ( -
{ - e.preventDefault() - const message = e.target[0].value - e.target[0].value = '' - message.startsWith('/') - ? runCommand(message.slice(1)) - : message.length > 0 && - user.sendMessage({ - text: message, - roomId: room.id, - }) - }} - > - user.isTypingIn({ roomId: room.id })} - /> - - - - ) : null +export class CreateMessageForm extends Component { + constructor(props) { + super(props) + + this.state = { + message: '', + cursorPostion: 0 + } + } + + render() { + const { runCommand, toggleEmojiPicker } = this.props.actions + const { user, room, message, isPickerShowing } = this.props.state + + return room.id ? ( +
{ + e.preventDefault() + const message = this.state.message + message.startsWith('/') + ? runCommand(message.slice(1)) + : message.length > 0 && + user.sendMessage({ + text: message, + roomId: room.id + }) + + this.setState({ + message: '' + }) + }} + > + { + this.messageInput = input + }} + placeholder="Type a Message.." + onInput={this.handleInput.bind(this)} + onClick={this.updateCursorPostion.bind(this)} + onKeyUp={e => { + if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { + this.updateCursorPostion(e) + } + }} + value={this.state.message} + /> + + + + + ) : null + } + + handleInput(e) { + const { user, room } = this.props.state + user.isTypingIn({ roomId: room.id }) + + this.setState({ + message: e.target.value, + cursorPostion: e.target.selectionStart + }) + } + + updateCursorPostion(e) { + this.setState({ + cursorPostion: e.target.selectionStart + }) + } + + addEmojiToMessage(emoji) { + let message = this.state.message + let newMessage = '' + let cursor = this.state.cursorPostion + let beforeCursorStr = '' + let afterCursorStr = '' + + beforeCursorStr = message.substring(0, cursor) + afterCursorStr = message.substr(cursor) + newMessage = `${beforeCursorStr}${emoji}${afterCursorStr}` + + // reset the cursor to be after the emoji + this.messageInput.setSelectionRange( + this.state.cursorPostion + 2, + this.state.cursorPostion + 2, + 0 + ) + + this.setState({ + cursorPostion: this.state.cursorPostion + 2, + message: newMessage + }) + } +} diff --git a/src/components/EmojiPicker/index.js b/src/components/EmojiPicker/index.js new file mode 100644 index 0000000..cdac12a --- /dev/null +++ b/src/components/EmojiPicker/index.js @@ -0,0 +1,42 @@ +import EmojiPicker from 'emoji-picker-react' +import style from './index.module.css' + +import React from 'react' +import JSEMOJI from 'emoji-js' + +let jsemoji = new JSEMOJI() + +const Picker = ({ + state: { isPickerShowing }, + actions: { toggleEmojiPicker, handleEmojiSelection }, +}) => ( +
{ + e.nativeEvent.stopImmediatePropagation() + }} + className={style.component} + > +
+ { + e.nativeEvent.stopImmediatePropagation() + const emoji = jsemoji.replace_colons(`:${emojiObj.name}:`) + handleEmojiSelection(emoji) + toggleEmojiPicker(!isPickerShowing) + }} + /> +
+ { + e.preventDefault() + toggleEmojiPicker(!isPickerShowing) + }} + /> +
+) + +export default Picker diff --git a/src/components/EmojiPicker/index.module.css b/src/components/EmojiPicker/index.module.css new file mode 100644 index 0000000..8f3c5f9 --- /dev/null +++ b/src/components/EmojiPicker/index.module.css @@ -0,0 +1,34 @@ +.component { + width: 2rem; + height: 2rem; + position: relative; +} + +.overlayWrapper { + height: 0; + overflow: hidden; + position: absolute; + bottom: 32px; + right: 0; + -webkit-transition: height .25s ease-in-out; /* Safari */ + transition: height .25s ease-in-out; +} + +.button { + width: 32px; + height: 32px; + position: absolute; + bottom: 0; + right: 0; + background-image: url("https://cdn.jsdelivr.net/emojione/assets/3.0/png/32/1f600.png"); + background-position: center center; + background-repeat: no-repeat; + background-size: contain; + border: none; + cursor: pointer; +} + +.show { + /* find a better way to set this height */ + height: 333px; +} diff --git a/src/index.js b/src/index.js index 4688580..0953144 100644 --- a/src/index.js +++ b/src/index.js @@ -28,6 +28,7 @@ class View extends React.Component { messages: {}, typing: {}, sidebarOpen: false, + isPickerShowing: false, userListOpen: window.innerWidth > 1000, } @@ -38,6 +39,7 @@ class View extends React.Component { setSidebar: sidebarOpen => this.setState({ sidebarOpen }), setUserList: userListOpen => this.setState({ userListOpen }), + toggleEmojiPicker: isPickerShowing => this.setState({ isPickerShowing }), // -------------------------------------- // User @@ -66,12 +68,13 @@ class View extends React.Component { ) }, - subscribeToRoom: room => + subscribeToRoom: room => { !this.state.user.roomSubscriptions[room.id] && - this.state.user.subscribeToRoom({ - roomId: room.id, - hooks: { onNewMessage: this.actions.addMessage }, - }), + this.state.user.subscribeToRoom({ + roomId: room.id, + hooks: { onNewMessage: this.actions.addMessage }, + }) + }, createRoom: options => this.state.user.createRoom(options).then(this.actions.joinRoom), @@ -172,7 +175,6 @@ class View extends React.Component { // -------------------------------------- // Notifications // -------------------------------------- - showNotification: message => { if ( 'Notification' in window && @@ -210,6 +212,14 @@ class View extends React.Component { window.history.replaceState(null, null, window.location.pathname) ChatManager(this, user) }) + + document.addEventListener( + 'click', + e => { + this.actions.toggleEmojiPicker(false) + }, + false + ) } render() { diff --git a/yarn.lock b/yarn.lock index c24f55b..4dc0526 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1063,7 +1063,7 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@pusher/chatkit@^0.7.3": +"@pusher/chatkit@^0.7.9": version "0.7.9" resolved "https://registry.yarnpkg.com/@pusher/chatkit/-/chatkit-0.7.9.tgz#34587299107baf55b36b3fe1c9870ea99e90a62a" dependencies: @@ -1611,7 +1611,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.22.0, babel-runtime@^6.25.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -2964,6 +2964,23 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +emoji-datasource@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/emoji-datasource/-/emoji-datasource-4.0.0.tgz#3fc9c0c2f4fb321d9291138819f6d100603d3e2f" + +emoji-js@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/emoji-js/-/emoji-js-3.4.0.tgz#dabdeda60c92d1948a5177e51ba9421d2029b052" + dependencies: + emoji-datasource "4.0.0" + +emoji-picker-react@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-2.0.4.tgz#a58edc3412d39726dbcd64a7811806ccc90d9b46" + dependencies: + babel-runtime "^6.25.0" + throttle-debounce "^1.0.1" + emoji-regex@^6.1.0: version "6.5.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" @@ -7955,6 +7972,10 @@ throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" +throttle-debounce@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-1.0.1.tgz#dad0fe130f9daf3719fdea33dc36a8e6ba7f30b5" + through2@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"