├── .travis.yml ├── src ├── .eslintrc ├── actions │ ├── typing │ │ ├── index.js │ │ └── isTyping.js │ ├── index.js │ ├── people │ │ ├── index.js │ │ ├── getMyData.js │ │ ├── leaveChat.js │ │ ├── editMyData.js │ │ ├── addPerson.js │ │ ├── removePerson.js │ │ └── getOtherPeople.js │ ├── chats │ │ ├── getChat.js │ │ ├── newChat.js │ │ ├── deleteChat.js │ │ ├── editChat.js │ │ ├── getOrCreateChat.js │ │ ├── index.js │ │ ├── getChats.js │ │ ├── getLatestChats.js │ │ └── getChatsBefore.js │ ├── auth.js │ └── messages │ │ ├── index.js │ │ ├── deleteMessage.js │ │ ├── getMessage.js │ │ ├── readMessage.js │ │ ├── editMessage.js │ │ ├── getMessages.js │ │ ├── getLatestMessages.js │ │ └── sendMessage.js ├── components │ ├── ChatEngine │ │ ├── ChatFeed │ │ │ ├── Triggers │ │ │ │ ├── index.js │ │ │ │ └── RenderTrigger.js │ │ │ ├── Steps │ │ │ │ ├── index.js │ │ │ │ ├── CreateChat │ │ │ │ │ └── index.js │ │ │ │ ├── AuthFail │ │ │ │ │ └── index.js │ │ │ │ └── IceBreaker │ │ │ │ │ └── index.js │ │ │ ├── NewMessageForm │ │ │ │ ├── isImage.js │ │ │ │ ├── SendButton.js │ │ │ │ ├── ImagesRow.js │ │ │ │ ├── FilesRow.js │ │ │ │ ├── FilePreview.js │ │ │ │ ├── Thumbnail.js │ │ │ │ ├── AttachmentsInput.js │ │ │ │ ├── MessageInput.js │ │ │ │ └── index.js │ │ │ ├── Messages │ │ │ │ ├── Bubble │ │ │ │ │ ├── Body.js │ │ │ │ │ ├── file.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── FileView.js │ │ │ │ │ └── Thumbnail.js │ │ │ │ ├── DatePartition │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── MessageFormSocial │ │ │ │ ├── FileRow.js │ │ │ │ ├── edit.js │ │ │ │ ├── ImagesInput.js │ │ │ │ ├── MessageInput.js │ │ │ │ └── index.js │ │ │ ├── IsTyping │ │ │ │ └── index.js │ │ │ ├── ChatHeader │ │ │ │ ├── ChatListDrawer.js │ │ │ │ ├── ChatSettingsDrawer.js │ │ │ │ └── index.js │ │ │ └── ScrollDownBar │ │ │ │ └── index.js │ │ ├── components │ │ │ ├── Avatar │ │ │ │ ├── index.js │ │ │ │ ├── Dot │ │ │ │ │ └── index.js │ │ │ │ └── Avatar │ │ │ │ │ └── index.js │ │ │ ├── Effects │ │ │ │ ├── index.js │ │ │ │ ├── Float │ │ │ │ │ ├── index.js │ │ │ │ │ └── useFloat.js │ │ │ │ └── Boop │ │ │ │ │ ├── index.js │ │ │ │ │ └── useBoop.js │ │ │ ├── Input │ │ │ │ ├── index.js │ │ │ │ ├── TextInput │ │ │ │ │ └── index.js │ │ │ │ └── AutocompleteInput │ │ │ │ │ └── index.js │ │ │ └── Button │ │ │ │ └── index.js │ │ ├── Utilities │ │ │ ├── colorMapping.js │ │ │ └── timezone.js │ │ ├── ChatSettings │ │ │ ├── PhotosSettings │ │ │ │ ├── Thumbnail.js │ │ │ │ └── index.js │ │ │ ├── OptionsSettings │ │ │ │ └── index.js │ │ │ ├── PeopleSettings │ │ │ │ ├── PersonForm │ │ │ │ │ ├── PersonOption │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── index.js │ │ │ │ └── PersonRow │ │ │ │ │ └── index.js │ │ │ ├── SettingsBlock │ │ │ │ └── index.js │ │ │ ├── ChatSettingsTop │ │ │ │ ├── TitleForm │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── ChatList │ │ │ ├── ChatLoader.js │ │ │ ├── ChatCard │ │ │ │ ├── Loading.js │ │ │ │ └── index.js │ │ │ ├── NewChatForm │ │ │ │ └── index.js │ │ │ └── index.js │ │ └── index.js │ ├── Sockets │ │ ├── index.js │ │ ├── ChatSocket │ │ │ └── index.js │ │ └── Socket │ │ │ ├── getOrCreateSession.js │ │ │ └── index.js │ ├── index.js │ └── Context │ │ └── index.js ├── index.test.js └── index.js ├── .eslintignore ├── example ├── src │ ├── Actions │ │ ├── index.js │ │ └── Accounts │ │ │ ├── index.js │ │ │ ├── logout.js │ │ │ └── login.js │ ├── Utilities │ │ └── history.js │ ├── index.js │ ├── App.test.js │ ├── Reducers │ │ ├── index.js │ │ └── AccountsReducer.js │ ├── consts.staging.txt │ ├── Pages │ │ ├── SupportEngine │ │ │ ├── App.js │ │ │ ├── index.js │ │ │ ├── Home │ │ │ │ └── index.js │ │ │ ├── SupportDashboard │ │ │ │ └── index.js │ │ │ ├── App.css │ │ │ └── SupportEngine │ │ │ │ ├── index.js │ │ │ │ ├── SupportWindow │ │ │ │ ├── ChatEngine.js │ │ │ │ └── index.js │ │ │ │ └── Avatar.js │ │ ├── ChatTutorial │ │ │ ├── index.js │ │ │ ├── ChatFeed │ │ │ │ ├── MyMessage.js │ │ │ │ ├── TheirMessage.js │ │ │ │ ├── MessageForm.js │ │ │ │ └── index.js │ │ │ └── App.css │ │ ├── UserSocketFeedPage │ │ │ └── index.js │ │ ├── DirectChatsPage │ │ │ └── index.js │ │ ├── ChatSocketPage │ │ │ └── index.js │ │ ├── index.js │ │ ├── GifPage │ │ │ └── index.js │ │ └── HomePage │ │ │ ├── ChatEngine.js │ │ │ └── index.js │ ├── consts.local.txt │ ├── index.css │ ├── App.js │ └── teams.css ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── README.md ├── tests │ ├── tests_conf │ │ └── nightwatch.conf.js │ ├── tests │ │ ├── testTutorial.js │ │ ├── testChatSocket.js │ │ ├── testDirectChats.js │ │ └── testChatEngine.js │ └── tests_pages │ │ └── homePage.js └── package.json ├── .editorconfig ├── .prettierrc ├── .snyk ├── .gitignore ├── tests ├── tests_conf │ └── nightwatch.conf.js ├── tests_pages │ └── chatPage.js └── tests │ └── testNewChat.js ├── .eslintrc ├── CONTRIBUTING.md ├── .github └── workflows │ └── deploy.yml ├── package.json └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /src/actions/typing/index.js: -------------------------------------------------------------------------------- 1 | import { isTyping } from './isTyping' 2 | 3 | export { 4 | isTyping 5 | } -------------------------------------------------------------------------------- /example/src/Actions/index.js: -------------------------------------------------------------------------------- 1 | export const SIGN_IN_OK = 'SIGN_IN_OK' 2 | export const SIGN_OUT = 'SIGN_OUT' 3 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatengine-io/react-chat-engine/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /example/src/Utilities/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history' 2 | 3 | 4 | export default createBrowserHistory(); 5 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Triggers/index.js: -------------------------------------------------------------------------------- 1 | import RenderTrigger from './RenderTrigger' 2 | 3 | export { 4 | RenderTrigger, 5 | } -------------------------------------------------------------------------------- /example/src/Actions/Accounts/index.js: -------------------------------------------------------------------------------- 1 | import { login } from './login' 2 | import { logout } from './logout' 3 | 4 | export { 5 | login, 6 | logout 7 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/components/Avatar/index.js: -------------------------------------------------------------------------------- 1 | import Avatar from './Avatar' 2 | import Dot from './Dot' 3 | 4 | export { 5 | Avatar, 6 | Dot 7 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/components/Effects/index.js: -------------------------------------------------------------------------------- 1 | import Boop from './Boop' 2 | import Float from './Float' 3 | 4 | export { 5 | Boop, 6 | Float, 7 | } -------------------------------------------------------------------------------- /src/components/Sockets/index.js: -------------------------------------------------------------------------------- 1 | import Socket from './Socket' 2 | import ChatSocket from './ChatSocket' 3 | 4 | export { 5 | Socket, 6 | ChatSocket, 7 | } -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import { ChatEngine } from '.' 2 | 3 | describe('ChatEngine', () => { 4 | it('is truthy', () => { 5 | expect(ChatEngine).toBeTruthy() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /example/src/Actions/Accounts/logout.js: -------------------------------------------------------------------------------- 1 | import * as str from '../index.js'; 2 | 3 | export const logout = () => (dispatch) => { 4 | dispatch({ 5 | type: str.SIGN_OUT 6 | }); 7 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export function getApiUrl(props) { 2 | if (props && props.development) { 3 | return 'http://127.0.0.1:8000' 4 | } 5 | return 'https://api.chatengine.io' 6 | } 7 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import App from './App' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | -------------------------------------------------------------------------------- /src/components/ChatEngine/components/Input/index.js: -------------------------------------------------------------------------------- 1 | import TextInput from './TextInput' 2 | import AutoCompleteInput from './AutocompleteInput' 3 | 4 | export { 5 | TextInput, 6 | AutoCompleteInput, 7 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Steps/index.js: -------------------------------------------------------------------------------- 1 | import AuthFail from './AuthFail' 2 | import CreateChat from './CreateChat' 3 | import IceBreaker from './IceBreaker' 4 | 5 | export { 6 | AuthFail, 7 | CreateChat, 8 | IceBreaker, 9 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | It is linked to the react-chat-engine package in the parent directory for development purposes. 4 | 5 | You can run `yarn install` and then `yarn start` to test your package. 6 | -------------------------------------------------------------------------------- /example/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /example/src/Reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import accountsReducer from './AccountsReducer.js' 4 | 5 | import { reducer as formReducer } from 'redux-form' 6 | 7 | export default combineReducers({ 8 | accounts: accountsReducer, 9 | form: formReducer, 10 | }) 11 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/NewMessageForm/isImage.js: -------------------------------------------------------------------------------- 1 | const images = ['jpg', 'jpeg', 'png', 'gif', 'tiff'] 2 | 3 | export const isImage = (fileName) => { 4 | const dotSplit = fileName.split('.') 5 | return dotSplit.length > 0 && images.indexOf(dotSplit[dotSplit.length - 1].toLowerCase()) !== -1 6 | } -------------------------------------------------------------------------------- /example/src/consts.staging.txt: -------------------------------------------------------------------------------- 1 | export const DEVELOPMENT = false 2 | 3 | export const ROOT_URL = 'https://api.chatengine.dev/' 4 | 5 | export const PROJECT_ID = 'aafaed6f-f0c4-4c9a-af5e-227eca8cf5a1' 6 | export const USER_NAME = '' 7 | export const USER_SECRET = '' 8 | 9 | export const CHAT_ID = 0 10 | export const CHAT_ACCESS_KEY = '' 11 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | SNYK-JS-QUILL-1245047: 6 | - '*': 7 | reason: All messages are sanitized by server 8 | expires: 2021-06-22T22:41:27.149Z 9 | patch: {} 10 | -------------------------------------------------------------------------------- /example/src/Pages/SupportEngine/App.js: -------------------------------------------------------------------------------- 1 | // import './App.css' 2 | 3 | import Home from './Home' 4 | import SupportDashboard from './SupportDashboard' 5 | 6 | 7 | function App() { 8 | const path = window.location.pathname 9 | if (path.indexOf('/support') === -1) { 10 | return 11 | } else { 12 | return 13 | } 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-chat-engine", 3 | "name": "react-chat-engine", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/Pages/SupportEngine/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Home from './Home' 4 | import SupportDashboard from './SupportDashboard' 5 | 6 | 7 | const App = () => { 8 | const path = window.location.pathname 9 | if (path.indexOf('/support') === -1) { 10 | return 11 | } else { 12 | return 13 | } 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /example/src/Pages/SupportEngine/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.css'; 3 | import { htmlCode } from './html' 4 | 5 | import SupportEngine from '../SupportEngine'; 6 | 7 | 8 | function Home() { 9 | return ( 10 |
11 |
12 | 13 | 14 |
15 | ); 16 | } 17 | 18 | export default Home; 19 | -------------------------------------------------------------------------------- /example/src/Pages/SupportEngine/SupportDashboard/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ChatEngine } from 'react-chat-engine' 3 | 4 | function SupportDashboard() { 5 | return ( 6 | 12 | ); 13 | } 14 | 15 | export default SupportDashboard; 16 | -------------------------------------------------------------------------------- /src/actions/people/index.js: -------------------------------------------------------------------------------- 1 | import { addPerson } from './addPerson' 2 | import { removePerson } from './removePerson' 3 | import { getOtherPeople } from './getOtherPeople' 4 | import { getMyData } from './getMyData' 5 | import { editMyData } from './editMyData' 6 | import { leaveChat } from './leaveChat' 7 | 8 | export { 9 | addPerson, 10 | removePerson, 11 | getOtherPeople, 12 | getMyData, 13 | editMyData, 14 | leaveChat, 15 | } -------------------------------------------------------------------------------- /example/tests/tests_conf/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "src_folders" : ["tests/tests"], 3 | "page_objects_path" : ["tests/tests_pages"], 4 | 5 | "webdriver" : { 6 | "start_process": true, 7 | "server_path": "node_modules/.bin/chromedriver", 8 | "port": 9515 9 | }, 10 | 11 | "test_settings" : { 12 | "default" : { 13 | "desiredCapabilities": { 14 | "browserName": "chrome" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | .dccache 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | *.log 27 | tests_output 28 | 29 | example/src/consts.js 30 | -------------------------------------------------------------------------------- /src/actions/typing/isTyping.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import * as str from '..' 4 | import { getHeaders } from '../auth' 5 | 6 | export function isTyping(props, chatId, callback) { 7 | axios.post( 8 | `${str.getApiUrl(props)}/chats/${chatId}/typing/`, 9 | {}, 10 | { headers: getHeaders(props) } 11 | ) 12 | 13 | .then((response) => { 14 | callback && callback(response.data) 15 | }) 16 | 17 | .catch((error) => {}); 18 | } -------------------------------------------------------------------------------- /src/components/Sockets/ChatSocket/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import SocketChild from './ChatSocket' 4 | 5 | const ChatSocket = (props) => { 6 | const [hide, setHide] = useState(false) 7 | 8 | function reRender() { 9 | setHide(true) 10 | setTimeout(() => setHide(false), 100) 11 | } 12 | 13 | return ( 14 |
{!hide && reRender()} />}
15 | ) 16 | } 17 | 18 | export default ChatSocket 19 | -------------------------------------------------------------------------------- /tests/tests_conf/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "src_folders" : ["tests/tests"], 3 | "page_objects_path" : ["tests/tests_pages"], 4 | 5 | "webdriver" : { 6 | "start_process": true, 7 | "server_path": "node_modules/.bin/chromedriver", 8 | "port": 9515 9 | }, 10 | 11 | "test_settings" : { 12 | "default" : { 13 | "desiredCapabilities": { 14 | "browserName": "chrome" 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | 3 | import ChatEngine from './ChatEngine' 4 | 5 | import { ChatEngineWrapper, ChatEngineContext } from './Context' 6 | 7 | const ChatEngineApp = (props) => { 8 | if (useContext(ChatEngineContext)) { 9 | return 10 | } else { 11 | return 12 | } 13 | 14 | } 15 | 16 | export default ChatEngineApp 17 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Messages/Bubble/Body.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Body = props => { 4 | let text = props.text ? props.text : '' 5 | text = text.replaceAll("

", "

").replaceAll("

", "
") 6 | text = text.replaceAll(" 10 | ) 11 | } 12 | 13 | export default Body -------------------------------------------------------------------------------- /src/actions/people/getMyData.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import * as str from '..' 4 | import { getHeaders } from '../auth' 5 | 6 | export function getMyData(props, callback) { 7 | axios.get( 8 | `${str.getApiUrl(props)}/users/me/`, 9 | { headers: getHeaders(props) } 10 | ) 11 | 12 | .then((response) => { 13 | callback && callback(response.data) 14 | }) 15 | 16 | .catch((error) => { 17 | console.log('Get Myself Error', error) 18 | }); 19 | } -------------------------------------------------------------------------------- /example/src/Reducers/AccountsReducer.js: -------------------------------------------------------------------------------- 1 | import * as str from '../Actions/index.js'; 2 | 3 | const initialState = {}; 4 | 5 | // TODO: Name this AccountsReducer or something 6 | export default function accountsReducer(state = initialState, action) { 7 | switch (action.type) { 8 | case str.SIGN_IN_OK: 9 | return {...action.payload} 10 | 11 | case str.SIGN_OUT: 12 | return initialState; 13 | 14 | default: 15 | return state; 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Messages/Bubble/file.js: -------------------------------------------------------------------------------- 1 | const images = ['jpg', 'jpeg', 'png', 'gif', 'tiff'] 2 | 3 | export const isImage = (fileName) => { 4 | const dotSplit = fileName.split('.') 5 | return dotSplit.length > 0 && images.indexOf(dotSplit[dotSplit.length - 1].toLowerCase()) !== -1 6 | } 7 | 8 | export const getFileName = (fileUrl) => { 9 | const slashSplit = fileUrl.split('/') 10 | const nameAndHash = slashSplit[slashSplit.length - 1] 11 | return nameAndHash.split('?')[0] 12 | } -------------------------------------------------------------------------------- /src/actions/chats/getChat.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getChat(props, chatId, callback) { 6 | axios.get( 7 | `${str.getApiUrl(props)}/chats/${chatId}/`, 8 | { headers: getHeaders(props) } 9 | ) 10 | 11 | .then((response) => { 12 | callback && callback(response.data) 13 | }) 14 | 15 | .catch((error) => { 16 | console.log('Get Chat Error', error) 17 | }); 18 | } -------------------------------------------------------------------------------- /src/actions/chats/newChat.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function newChat(props, data, callback) { 6 | axios.post( 7 | `${str.getApiUrl(props)}/chats/`, 8 | data, 9 | { headers: getHeaders(props) } 10 | ) 11 | 12 | .then((response) => { 13 | callback && callback(response.data) 14 | }) 15 | 16 | .catch((error) => { 17 | console.log('New Chat Error', error) 18 | }); 19 | } -------------------------------------------------------------------------------- /src/actions/people/leaveChat.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '..' 3 | import { getHeaders } from '../auth' 4 | 5 | export function leaveChat(props, chatId, callback) { 6 | axios.delete( 7 | `${str.getApiUrl(props)}/chats/${chatId}/people/`, 8 | { headers: getHeaders(props)} 9 | ) 10 | 11 | .then((response) => { 12 | callback && callback(response.data) 13 | }) 14 | 15 | .catch((error) => { 16 | console.log('Delete Person Error', error) 17 | }); 18 | } -------------------------------------------------------------------------------- /src/actions/chats/deleteChat.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function deleteChat(props, chatId, callback) { 6 | axios.delete( 7 | `${str.getApiUrl(props)}/chats/${chatId}/`, 8 | { headers: getHeaders(props) } 9 | ) 10 | 11 | .then((response) => { 12 | callback && callback(response.data) 13 | }) 14 | 15 | .catch((error) => { 16 | console.log('Delete Chat Error', error) 17 | }); 18 | } -------------------------------------------------------------------------------- /src/actions/people/editMyData.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import * as str from '..' 4 | import { getHeaders } from '../auth' 5 | 6 | export function editMyData(props, data, callback) { 7 | axios.patch( 8 | `${str.getApiUrl(props)}/chats/me/`, 9 | data, 10 | { headers: getHeaders(props) } 11 | ) 12 | 13 | .then((response) => { 14 | callback && callback(response.data) 15 | }) 16 | 17 | .catch((error) => { 18 | console.log('Edit Myself Error', error) 19 | }); 20 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/components/Effects/Float/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { animated } from 'react-spring'; 3 | import useFloat from './useFloat'; 4 | 5 | const Float = ({ children, triggers=[], ...floatConfig }) => { 6 | const [style, trigger] = useFloat(floatConfig); 7 | 8 | return ( 9 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default Float -------------------------------------------------------------------------------- /example/src/Pages/SupportEngine/App.css: -------------------------------------------------------------------------------- 1 | .ce-my-message-bubble { 2 | background-color: #7a39e0 !important; 3 | } 4 | 5 | .ce-chat-title-text { 6 | color: #7a39e0 !important; 7 | } 8 | 9 | .ce-chat-subtitle-text { 10 | color: #7a39e0 !important; 11 | font-weight: 600; 12 | } 13 | 14 | #ce-send-message-button { 15 | position: relative !important; 16 | bottom: 2px !important; 17 | left: 1px !important; 18 | background-color: #7a39e0 !important; 19 | } 20 | 21 | .ce-my-message-timestamp { 22 | color: #7a39e0 !important; 23 | } 24 | -------------------------------------------------------------------------------- /example/src/consts.local.txt: -------------------------------------------------------------------------------- 1 | export const DEVELOPMENT = true 2 | 3 | export const ROOT_URL = DEVELOPMENT ? '' : 'https://api.chatengine.io/' 4 | 5 | export const PROJECT_ID = DEVELOPMENT ? '1ed59673-1fd6-46ed-9eb9-56239a6a4f82' : '077b8cbe-d06d-4a92-9d3f-6d13a0fa6c40' 6 | export const USER_NAME = DEVELOPMENT ? 'Adam_La_Morre' : '' 7 | export const USER_SECRET = DEVELOPMENT ? 'pass1234' : '' 8 | 9 | export const CHAT_ID = DEVELOPMENT ? 289 : 0 10 | export const CHAT_ACCESS_KEY = DEVELOPMENT ? 'ca-0d21f8cb-b884-4a8b-9e2e-a2acbdbc3792' : '' 11 | -------------------------------------------------------------------------------- /src/actions/chats/editChat.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function editChat(props, chatId, data, callback) { 6 | axios.patch( 7 | `${str.getApiUrl(props)}/chats/${chatId}/`, 8 | data, 9 | { headers: getHeaders(props) } 10 | ) 11 | 12 | .then((response) => { 13 | callback && callback(response.data) 14 | }) 15 | 16 | .catch((error) => { 17 | console.log('Edit Chat Error', error) 18 | }); 19 | } -------------------------------------------------------------------------------- /src/actions/chats/getOrCreateChat.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getOrCreateChat(props, data, callback) { 6 | axios.put( 7 | `${str.getApiUrl(props)}/chats/`, 8 | data, 9 | { headers: getHeaders(props) } 10 | ) 11 | 12 | .then((response) => { 13 | callback && callback(response.data) 14 | }) 15 | 16 | .catch((error) => { 17 | console.log('Get or Create Chat Error', error) 18 | }); 19 | } -------------------------------------------------------------------------------- /example/tests/tests/testTutorial.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "tags": ["home", "JSM", "tutorial"], 3 | 4 | 'Try the JSM tutorial 1': function(browser) { 5 | const page = browser.page.homePage(); 6 | 7 | page 8 | .navigate('http://localhost:3001/tutorial') 9 | .pause(1500) 10 | 11 | // Send a DM 12 | .waitForElementVisible('@newChatButton', 1000, 'Find new chat button') 13 | .click('@newChatButton') 14 | .waitForElementVisible('@newChatInput', 1000, 'Find new chat input'); 15 | }, 16 | } -------------------------------------------------------------------------------- /src/actions/auth.js: -------------------------------------------------------------------------------- 1 | export function getHeaders(props) { 2 | if (!props) { 3 | return 4 | } else if (props.chatID) { 5 | return { 6 | 'public-key': props.publicKey ? props.publicKey : props.projectID, 7 | 'chat-id': props.chatID, 8 | 'access-key': props.chatAccessKey 9 | } 10 | } else { 11 | return { 12 | 'Public-Key': props.publicKey ? props.publicKey : props.projectID, 13 | 'User-Name': props.userName, 14 | 'User-Secret': props.userPassword ? props.userPassword : props.userSecret 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/actions/messages/index.js: -------------------------------------------------------------------------------- 1 | import { getMessages } from './getMessages' 2 | import { getLatestMessages } from './getLatestMessages' 3 | import { sendMessage } from './sendMessage' 4 | 5 | import { getMessage } from './getMessage' 6 | import { editMessage } from './editMessage' 7 | import { readMessage } from './readMessage' 8 | import { deleteMessage } from './deleteMessage' 9 | 10 | export { 11 | getMessages, 12 | getLatestMessages, 13 | sendMessage, 14 | 15 | getMessage, 16 | editMessage, 17 | readMessage, 18 | deleteMessage, 19 | } -------------------------------------------------------------------------------- /src/actions/messages/deleteMessage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function deleteMessage(props, chatId, messageId, callback) { 6 | axios.delete( 7 | `${str.getApiUrl(props)}/chats/${chatId}/messages/${messageId}/`, 8 | { headers: getHeaders(props) } 9 | ) 10 | 11 | .then((response) => { 12 | callback && callback(response.data) 13 | }) 14 | 15 | .catch((error) => { 16 | console.log('Delete Messages Error', error) 17 | }); 18 | } -------------------------------------------------------------------------------- /src/actions/messages/getMessage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getMessage(props, chatId, messageId, callback) { 6 | axios.get( 7 | `${str.getApiUrl(props)}/chats/${chatId}/messages/${messageId}/`, 8 | { headers: getHeaders(props) } 9 | ) 10 | 11 | .then((response) => { 12 | callback && callback(chatId, response.data) 13 | }) 14 | 15 | .catch((error) => { 16 | console.log('Fetch Message Error', error) 17 | }); 18 | } -------------------------------------------------------------------------------- /src/actions/people/addPerson.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import * as str from '..' 4 | import { getHeaders } from '../auth' 5 | 6 | export function addPerson(props, chatId, userName, callback) { 7 | axios.post( 8 | `${str.getApiUrl(props)}/chats/${chatId}/people/`, 9 | { username: userName }, 10 | { headers: getHeaders(props) } 11 | ) 12 | 13 | .then((response) => { 14 | callback && callback(response.data) 15 | }) 16 | 17 | .catch((error) => { 18 | console.log('New Person Error', error) 19 | }); 20 | } -------------------------------------------------------------------------------- /src/actions/people/removePerson.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '..' 3 | import { getHeaders } from '../auth' 4 | 5 | export function removePerson(props, chatId, userName, callback) { 6 | axios.put( 7 | `${str.getApiUrl(props)}/chats/${chatId}/people/`, 8 | { username: userName }, 9 | { headers: getHeaders(props) } 10 | ) 11 | 12 | .then((response) => { 13 | callback && callback(response.data) 14 | }) 15 | 16 | .catch((error) => { 17 | console.log('Delete Person Error', error) 18 | }); 19 | } -------------------------------------------------------------------------------- /src/actions/chats/index.js: -------------------------------------------------------------------------------- 1 | import { getChats } from './getChats' 2 | import { newChat } from './newChat' 3 | import { getLatestChats } from './getLatestChats' 4 | import { getChatsBefore } from './getChatsBefore' 5 | import { getOrCreateChat } from './getOrCreateChat' 6 | import { getChat } from './getChat' 7 | import { editChat } from './editChat' 8 | import { deleteChat } from './deleteChat' 9 | 10 | export { 11 | getChats, 12 | newChat, 13 | getLatestChats, 14 | getChatsBefore, 15 | getOrCreateChat, 16 | getChat, 17 | editChat, 18 | deleteChat, 19 | } -------------------------------------------------------------------------------- /src/actions/messages/readMessage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function readMessage(props, chatId, messageId, callback) { 6 | axios.patch( 7 | `${str.getApiUrl(props)}/chats/${chatId}/people/`, 8 | { last_read: messageId }, 9 | { headers: getHeaders(props) } 10 | ) 11 | 12 | .then((response) => { 13 | callback && callback(response.data) 14 | }) 15 | 16 | .catch((error) => { 17 | console.log('Read Message Error', error) 18 | }); 19 | } -------------------------------------------------------------------------------- /src/actions/messages/editMessage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function editMessage(props, chatId, messageId, data, callback) { 6 | axios.patch( 7 | `${str.getApiUrl(props)}/chats/${chatId}/messages/${messageId}/`, 8 | data, 9 | { headers: getHeaders(props) } 10 | ) 11 | 12 | .then((response) => { 13 | callback && callback(response.data) 14 | }) 15 | 16 | .catch((error) => { 17 | console.log('Delete Messages Error', error) 18 | }); 19 | } -------------------------------------------------------------------------------- /src/components/Sockets/Socket/getOrCreateSession.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../../actions' 3 | import { getHeaders } from '../../../actions/auth' 4 | 5 | export function getOrCreateSession(props, callback, errorFunc) { 6 | axios 7 | .get(`${str.getApiUrl(props)}/users/me/session/`, { 8 | headers: getHeaders(props) 9 | }) 10 | 11 | .then((response) => { 12 | callback && callback(response.data) 13 | }) 14 | 15 | .catch((error) => { 16 | console.log('Get or Create Session Error', error) 17 | errorFunc && errorFunc(error) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/actions/chats/getChats.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getChats(props, callback) { 6 | axios.get( 7 | `${str.getApiUrl(props)}/chats/`, 8 | { headers: getHeaders(props) } 9 | ) 10 | 11 | .then((response) => { 12 | // Run hook in Axios on GET requests 13 | props.onGetChats && props.onGetChats(response.data) 14 | 15 | callback && callback(response.data) 16 | }) 17 | 18 | .catch((error) => { 19 | console.log('Fetch Chats Error', error) 20 | }); 21 | } -------------------------------------------------------------------------------- /src/actions/chats/getLatestChats.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getLatestChats(props, count, callback) { 6 | axios.get( 7 | `${str.getApiUrl(props)}/chats/latest/${count}/`, 8 | { headers: getHeaders(props) } 9 | ) 10 | 11 | .then((response) => { 12 | // Run hook in Axios on GET requests 13 | props.onGetChats && props.onGetChats(response.data) 14 | 15 | callback && callback(response.data) 16 | }) 17 | 18 | .catch((error) => { 19 | console.log('Fetch Chats Error', error) 20 | }); 21 | } -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | } 7 | 8 | ::-webkit-scrollbar { 9 | width: 0; /* Remove scrollbar space */ 10 | background: transparent; /* Optional: just make scrollbar invisible */ 11 | } 12 | 13 | * { 14 | font-family: Avenir, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, 15 | Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, 16 | Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 17 | } 18 | 19 | code { 20 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 21 | monospace; 22 | } 23 | -------------------------------------------------------------------------------- /tests/tests_pages/chatPage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | url: 'http://localhost:3001', 3 | elements: { 4 | landingPage: '#home-section', 5 | }, 6 | commands: [{ 7 | assertText(element, expectedText, assertText){ 8 | return this 9 | .getText(element, function(result) { 10 | this.assert.equal( 11 | result.value, 12 | expectedText, 13 | assertText 14 | ); 15 | }) 16 | }, 17 | set(element, value){ 18 | return this 19 | .click(element) 20 | .clearValue(element) 21 | .setValue(element, value) 22 | } 23 | }] 24 | } -------------------------------------------------------------------------------- /src/actions/messages/getMessages.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getMessages(props, chatId, callback) { 6 | axios.get( 7 | `${str.getApiUrl(props)}/chats/${chatId}/messages/`, 8 | { headers: getHeaders(props) } 9 | ) 10 | 11 | .then((response) => { 12 | // Run hook in Axios on GET requests 13 | props.onGetMessages && props.onGetMessages(chatId, response.data) 14 | 15 | callback && callback(chatId, response.data) 16 | }) 17 | 18 | .catch((error) => { 19 | console.log('Fetch Messages Error', error) 20 | }); 21 | } -------------------------------------------------------------------------------- /tests/tests/testNewChat.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "tags": ["auth", "authentication", 'login'], 3 | 4 | 'Login to logout': function(browser) { 5 | const page = browser.page.chatPage(); 6 | 7 | page 8 | // Go to home 9 | .pause(3000) 10 | }, 11 | 12 | 'Login bad username': function(browser) { 13 | const page = browser.page.chatPage(); 14 | 15 | page 16 | // Go to home 17 | .pause(3000) 18 | }, 19 | 20 | 'Login bad password': function(browser) { 21 | const page = browser.page.chatPage(); 22 | 23 | page 24 | // Go to home 25 | .pause(3000) 26 | }, 27 | } -------------------------------------------------------------------------------- /src/actions/chats/getChatsBefore.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getChatsBefore(props, before, count, callback) { 6 | axios.put( 7 | `${str.getApiUrl(props)}/chats/latest/${count}/`, 8 | { before }, 9 | { headers: getHeaders(props) } 10 | ) 11 | 12 | .then((response) => { 13 | // Run hook in Axios on GET requests 14 | props.onGetChats && props.onGetChats(response.data) 15 | 16 | callback && callback(response.data) 17 | }) 18 | 19 | .catch((error) => { 20 | console.log('Fetch Chats Before Error', error) 21 | }); 22 | } -------------------------------------------------------------------------------- /src/actions/messages/getLatestMessages.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getLatestMessages(props, chatId, count, callback) { 6 | if (!getHeaders(props)) return; 7 | 8 | axios.get( 9 | `${str.getApiUrl(props)}/chats/${chatId}/messages/latest/${count}/`, 10 | { headers: getHeaders(props) } 11 | ) 12 | 13 | .then((response) => { 14 | props.onGetMessages && props.onGetMessages(chatId, response.data) 15 | 16 | callback && callback(chatId, response.data) 17 | }) 18 | 19 | .catch((error) => { 20 | console.log('Fetch Latest Messages Error', error) 21 | }); 22 | } -------------------------------------------------------------------------------- /example/src/Actions/Accounts/login.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as str from '../index.js'; 3 | 4 | export const login = (data, successFunc, errorFunc) => (dispatch) => { 5 | axios.get( 6 | `${data.rootUrl}users/me/`, 7 | { headers: { 8 | 'Project-ID': data.projectID, 9 | 'User-Name': data.userName, 10 | 'User-Secret': data.userSecret, 11 | }} 12 | ) 13 | 14 | .then((response) => { 15 | successFunc && successFunc(response) 16 | 17 | dispatch({ 18 | type: str.SIGN_IN_OK, 19 | payload: data 20 | }); 21 | }) 22 | 23 | .catch((error) => { 24 | errorFunc && errorFunc(error) 25 | }); 26 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react" 9 | ], 10 | "env": { 11 | "node": true 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2020, 15 | "ecmaFeatures": { 16 | "legacyDecorators": true, 17 | "jsx": true 18 | } 19 | }, 20 | "settings": { 21 | "react": { 22 | "version": "16" 23 | } 24 | }, 25 | "rules": { 26 | "space-before-function-paren": 0, 27 | "react/prop-types": 0, 28 | "react/jsx-handler-names": 0, 29 | "react/jsx-fragments": 0, 30 | "react/no-unused-prop-types": 0, 31 | "import/export": 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/actions/people/getOtherPeople.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function getOtherPeople(props, chatId, successCallback, errorCallback) { 6 | axios.get( 7 | `${str.getApiUrl(props)}/chats/${chatId}/others/`, 8 | { headers: getHeaders(props) } 9 | ) 10 | 11 | .then((response) => { 12 | // Run hook in Axios on GET requests 13 | props.onGetOtherPeople && props.onGetOtherPeople(chatId, response.data) 14 | 15 | successCallback && successCallback(chatId, response.data) 16 | }) 17 | 18 | .catch((error) => { 19 | console.log('Fetch Other People Error', error) 20 | 21 | errorCallback && errorCallback() 22 | }); 23 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/components/Effects/Boop/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { animated } from 'react-spring'; 3 | import useBoop from './useBoop'; 4 | 5 | const Boop = ({ children, triggers=[], ...boopConfig }) => { 6 | const [style, trigger] = useBoop(boopConfig); 7 | 8 | function isTriggerPresent(trigger) { 9 | return triggers.indexOf(trigger) !== -1 10 | } 11 | 12 | return ( 13 | isTriggerPresent('onClick') && trigger()} 16 | onMouseEnter={() => isTriggerPresent('onMouseEnter') && trigger()} 17 | onMouseLeave={() => isTriggerPresent('onMouseLeave') && trigger()} 18 | > 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default Boop -------------------------------------------------------------------------------- /src/components/ChatEngine/Utilities/colorMapping.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const colors = [ 4 | '#D64045', 5 | '#5B3000', 6 | '#00CC99', 7 | '#467599', 8 | '#1D3354', 9 | '#8F250C', 10 | '#6153CC', 11 | '#961D4E', 12 | '#A29F15', 13 | '#0CAADC', 14 | '#FF5154', 15 | '#FA7921', 16 | '#688E26', 17 | '#550527', 18 | '#A10702', 19 | '#FF1053', 20 | '#6C6EA0', 21 | '#100B00', 22 | ] 23 | 24 | function stringToNumber(str){ 25 | let sum = 0 26 | for (var i = 0; i < str.length; i++) { 27 | sum = sum + (str.charCodeAt(i) * i) - 97 28 | } 29 | return sum 30 | } 31 | 32 | export function stringToColor(str) { 33 | if (!str) { 34 | return 'black' 35 | } else { 36 | return colors[stringToNumber(str) % colors.length] 37 | } 38 | } -------------------------------------------------------------------------------- /example/src/Pages/ChatTutorial/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { ChatEngine } from 'react-chat-engine' 4 | 5 | import ChatFeed from './ChatFeed' 6 | 7 | // import './App.css' 8 | 9 | const userNames = ['Alice', 'Bob', 'Wendy', 'Zack'] 10 | const userName = userNames[Math.floor(Math.random() * userNames.length)] 11 | 12 | export default class App extends Component { 13 | render() { 14 | return ( 15 | } 21 | onNewMessage={() => new Audio('https://chat-engine-assets.s3.amazonaws.com/click.mp3').play()} 22 | /> 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/MessageFormSocial/FileRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Thumbnail from './Thumbnail' 4 | 5 | const FileRow = props => { 6 | function renderFiles() { 7 | return props.files.map((file, index) => { 8 | return ( 9 | props.onRemove && props.onRemove(index)} 13 | /> 14 | ) 15 | }) 16 | } 17 | 18 | return ( 19 |
25 | { renderFiles() } 26 |
27 | ) 28 | } 29 | 30 | export default FileRow -------------------------------------------------------------------------------- /example/src/Pages/ChatTutorial/ChatFeed/MyMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function MyMessage(props) { 4 | const { message } = props 5 | 6 | if (message.attachments && message.attachments.length > 0) { 7 | return ( 8 | message-attachment 14 | ) 15 | } 16 | 17 | return ( 18 |
27 | { message.text } 28 |
29 | ); 30 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/NewMessageForm/SendButton.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { ArrowUpOutlined } from '@ant-design/icons' 4 | 5 | const SendButton = () => { 6 | const [hover, setHover] = useState(false) 7 | 8 | return ( 9 |
10 |
setHover(true)} 13 | onMouseLeave={() => setHover(false)} 14 | style={{ 15 | cursor: 'pointer', 16 | backgroundColor: hover ? '#40a9ff' : '#1890ff', 17 | display: 'inline-block', 18 | padding: '5px 9px', 19 | borderRadius: '8px', 20 | }} 21 | > 22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default SendButton 29 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Messages/DatePartition/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { getDateTime, formatDateTime } from '../../../Utilities/timezone' 3 | 4 | const DatePartition = props => { 5 | const { lastCreated, created } = props 6 | 7 | function getDate(date) { 8 | return date ? date.substr(0, 10) : null 9 | } 10 | 11 | const lastDate = getDate(lastCreated) 12 | const thisDate = getDate(created) 13 | 14 | if (lastCreated && lastDate === thisDate) return
15 | 16 | return ( 17 |
18 | { formatDateTime(getDateTime(created, props.offset)) } 19 |
20 | ) 21 | } 22 | 23 | export default DatePartition 24 | 25 | const styles = { 26 | dateText: { 27 | width: '100%', 28 | textAlign: 'center', 29 | paddingTop: '4px', 30 | paddingBottom: '10px', 31 | fontSize: '15px', 32 | color: 'rgba(0, 0, 0, .40)' 33 | } 34 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/NewMessageForm/ImagesRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Thumbnail from './Thumbnail' 4 | 5 | import { isImage } from './isImage' 6 | 7 | const ImagesRow = props => { 8 | function renderFiles() { 9 | return props.files.map((file, index) => { 10 | if(isImage(file.name)) { 11 | return ( 12 | props.onRemove && props.onRemove(index)} 16 | /> 17 | ) 18 | } else { 19 | return
20 | } 21 | }) 22 | } 23 | 24 | return ( 25 |
29 | { renderFiles() } 30 |
31 | ) 32 | } 33 | 34 | export default ImagesRow -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/MessageFormSocial/edit.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { editMessage } from 'react-chat-engine' 4 | 5 | import { Button, TextInput } from 'react-chat-engine' 6 | 7 | const MessageEditForm = props => { 8 | const [value, setValue] = useState('') 9 | 10 | function handleChange(event) { 11 | setValue(event.target.value); 12 | } 13 | 14 | function handleSubmit(event) { 15 | event.preventDefault(); 16 | 17 | editMessage( 18 | props.conn, 19 | props.chatId, 20 | props.message.id, 21 | { text: value }, 22 | (data) => { } 23 | ) 24 | } 25 | return ( 26 |
27 | 34 |
31 | 32 |
33 | ) 34 | } 35 | 36 | export default OptionsSettings 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Chat Engine 2 | 3 | So you're interested in helping out, sweet! 4 | 5 | Let's show you what you need to get developer access to the project: 6 | 7 | ### 1. Create a consts/index.js file. 8 | 9 | Create a example/src/consts/index.js file with the following contents. NOTE: Replace the values with the correct parts of _your_ Chat Engine project. 10 | 11 | ``` 12 | export const DEVELOPMENT = false 13 | 14 | export const ROOT_URL = 'https://api.chatengine.io/' 15 | 16 | export const PROJECT_ID = prod ? '00000000-0000-0000-0000-000000000000' 17 | export const USER_NAME = 'Adam_La_Morre' 18 | export const USER_SECRET = 'pass1234' 19 | 20 | export const CHAT_ID = 1 21 | export const CHAT_ACCESS_KEY = 'ca-00000000-0000-0000-0000-000000000000' 22 | 23 | ``` 24 | 25 | ### 2. Start the project 26 | 27 | In the top-level dir, run `yarn` to install deps, then `yarn start` to start the ongiong build. 28 | 29 | Additionally the example dir is where we test the newest react-chat-engine instance. Run `yarn` to install deps for the example projects, then run `yarn start` to start the app in the browser. 30 | 31 | 34 | -------------------------------------------------------------------------------- /example/src/Pages/DirectChatsPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { ChatEngine, getOrCreateChat } from 'react-chat-engine' 4 | 5 | import { DEVELOPMENT, PROJECT_ID, USER_NAME, USER_SECRET } from '../../consts' 6 | 7 | const DirectChatPage = () => { 8 | const [username, setUsername] = useState('') 9 | 10 | function createDirectChat(creds) { 11 | getOrCreateChat( 12 | creds, 13 | { is_direct_chat: true, usernames: [username] }, 14 | () => setUsername('') 15 | ) 16 | } 17 | 18 | function renderChatForm(creds) { 19 | return ( 20 |
21 | setUsername(e.target.value)} /> 22 | 23 |
24 | ) 25 | } 26 | 27 | return ( 28 | renderChatForm(creds)} 35 | /> 36 | ) 37 | } 38 | 39 | export default DirectChatPage -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Triggers/RenderTrigger.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect, useContext } from 'react' 2 | 3 | import { ChatEngineContext } from '../../../Context' 4 | 5 | const RenderTrigger = props => { 6 | const { setLoadMoreMessages } = useContext(ChatEngineContext) 7 | 8 | function useOnScreen(ref) { 9 | const [isIntersecting, setIntersecting] = useState(false) 10 | 11 | const observer = new IntersectionObserver( 12 | ([entry]) => { 13 | setIntersecting(entry.isIntersecting) 14 | 15 | if (entry.isIntersecting) { 16 | props.onEnter && props.onEnter() 17 | } else { 18 | props.onLeave && props.onLeave() 19 | } 20 | } 21 | ) 22 | 23 | useEffect(() => { 24 | observer.observe(ref.current) 25 | // Remove the observer as soon as the component is unmounted 26 | return () => { observer.disconnect() } 27 | }, []) 28 | 29 | return isIntersecting 30 | } 31 | 32 | const ref = useRef() 33 | const isVisible = useOnScreen(ref) 34 | 35 | return
{props.children}
36 | } 37 | 38 | export default RenderTrigger -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatList/ChatLoader.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react' 2 | 3 | import { LoadingOutlined } from '@ant-design/icons' 4 | 5 | const ChatLoader = props => { 6 | function useOnScreen(ref) { 7 | const [isIntersecting, setIntersecting] = useState(false) 8 | 9 | const observer = new IntersectionObserver( 10 | ([entry]) => { 11 | setIntersecting(entry.isIntersecting) 12 | if (entry.isIntersecting) { 13 | props.onVisible() 14 | } 15 | } 16 | ) 17 | 18 | useEffect(() => { 19 | observer.observe(ref.current) 20 | // Remove the observer as soon as the component is unmounted 21 | return () => { observer.disconnect() } 22 | }, []) 23 | 24 | return isIntersecting 25 | } 26 | 27 | const ref = useRef() 28 | const isVisible = useOnScreen(ref) 29 | 30 | return ( 31 |
32 |
33 | 34 |
35 |
36 | ) 37 | } 38 | 39 | export default ChatLoader -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatSettings/PeopleSettings/PersonForm/PersonOption/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Avatar } from 'react-chat-engine' 5 | 6 | 7 | const PersonOption = props => { 8 | const [focused, setFocused] = useState(false) 9 | 10 | const { avatar, username } = props.person 11 | 12 | return ( 13 |
setFocused(true)} 16 | onMouseLeave={() => setFocused(false)} 17 | onClick={() => props.onClick && props.onClick()} 18 | style={{ ...styles.option, ...{ backgroundColor: focused ? '#f5f5f5' : 'white'} }} 19 | > 20 |
21 | 22 |
23 | 24 |
25 | {props.person.username} 26 |
27 |
28 | ); 29 | } 30 | 31 | export default PersonOption 32 | 33 | const styles = { 34 | option: { 35 | padding: '4px 16px', 36 | cursor: 'pointer', 37 | fontSize: '15px', 38 | display: 'flex' 39 | }, 40 | } 41 | 42 | PersonOption.propTypes = { 43 | person: PropTypes.object.isRequired, 44 | } -------------------------------------------------------------------------------- /src/actions/messages/sendMessage.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as str from '../../actions' 3 | import { getHeaders } from '../auth' 4 | 5 | export function sendMessage(props, chatId, data, callback) { 6 | let formdata = new FormData() 7 | 8 | if (data.attachments) { 9 | for (let i = 0; i < data.attachments.length; i++) { 10 | formdata.append('attachments', data.attachments[i], data.attachments[i].name) 11 | } 12 | } else if (data.files) { 13 | for (let i = 0; i < data.files.length; i++) { 14 | formdata.append('attachments', data.files[i], data.files[i].name) 15 | } 16 | } 17 | 18 | if (data.created) { 19 | formdata.append('created', data.created) 20 | } 21 | 22 | formdata.append('text', data.text) 23 | formdata.append('sender_username', data.sender_username) 24 | formdata.append('custom_json', JSON.stringify(data.custom_json ? data.custom_json : {})) 25 | 26 | axios.post( 27 | `${str.getApiUrl(props)}/chats/${chatId}/messages/`, 28 | formdata, 29 | { headers: getHeaders(props) } 30 | ) 31 | 32 | .then((response) => { 33 | callback && callback(response.data) 34 | }) 35 | 36 | .catch((error) => { 37 | console.log('Send Messages Error', error) 38 | }); 39 | 40 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/NewMessageForm/FilePreview.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { FileOutlined } from '@ant-design/icons' 4 | 5 | import { CloseCircleTwoTone } from '@ant-design/icons' 6 | 7 | const FilePreview = props => { 8 | const [hovered, setHovered] = useState(false) 9 | 10 | return ( 11 |
setHovered(true)} 13 | onMouseLeave={() => setHovered(false)} 14 | style={{ 15 | ...styles.filePreview, 16 | ...{ paddingRight: hovered ? '6px' : '26px' } 17 | }} 18 | > 19 | {' '}{ props.file && props.file.name } {' '} 20 | 21 | { 22 | hovered && 23 | props.onRemove && props.onRemove()} 26 | /> 27 | } 28 |
29 | ) 30 | } 31 | 32 | export default FilePreview 33 | 34 | const styles = { 35 | filePreview: { 36 | padding: '12px', 37 | display: 'inline-block', 38 | position: "relative", 39 | border: '1px solid #40a9ff', 40 | color: '#434343', 41 | borderRadius: '14px' 42 | }, 43 | closeIcon: { 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/src/teams.css: -------------------------------------------------------------------------------- 1 | /* TEAMS */ 2 | 3 | .ce-chat-list { 4 | background-color: rgb(240, 240, 240) !important; 5 | } 6 | 7 | .ce-chats-container { 8 | background-color: rgb(240, 240, 240) !important; 9 | } 10 | 11 | .ce-active-chat-card { 12 | background-color: #cabcdc !important; 13 | border: 4px solid #cabcdc !important; 14 | border-radius: 0px !important; 15 | } 16 | 17 | .ce-chat-subtitle-text { 18 | color: #595959 !important; 19 | } 20 | 21 | .ce-text-input { 22 | border-radius: 6px !important; 23 | border: 1px solid #3b2a50 !important; 24 | } 25 | 26 | .ce-primary-button { 27 | border-radius: 6px !important; 28 | background-color: #7554a0 !important; 29 | position: relative; 30 | bottom: 1px; 31 | } 32 | 33 | .ce-danger-button { 34 | background-color: white !important; 35 | border-radius: 22px !important; 36 | } 37 | 38 | .ce-settings { 39 | background-color: rgb(240, 240, 240) !important; 40 | } 41 | 42 | .ce-autocomplete-input { 43 | border-radius: 6px !important; 44 | border: 1px solid #3b2a50 !important; 45 | } 46 | 47 | .ce-autocomplete-options { 48 | border-radius: 6px !important; 49 | border: 1px solid #3b2a50 !important; 50 | background-color: white !important; 51 | } 52 | 53 | .ce-chat-settings-container { 54 | padding-top: 12px !important; 55 | } 56 | 57 | .ce-chat-avatars-row { 58 | display: none !important; 59 | } 60 | -------------------------------------------------------------------------------- /example/src/Pages/SupportEngine/SupportEngine/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | 3 | import SupportWindow from './SupportWindow' 4 | 5 | import Avatar from './Avatar' 6 | 7 | const SupportEngine = () => { 8 | const wrapperRef = useRef(null); 9 | useOutsideAlerter(wrapperRef); 10 | const [visible, setVisible] = useState(false) 11 | 12 | function useOutsideAlerter(ref) { 13 | useEffect(() => { 14 | function handleClickOutside(event) { 15 | if (ref.current && !ref.current.contains(event.target)) { 16 | setVisible(false) 17 | } 18 | } 19 | 20 | document.addEventListener("mousedown", handleClickOutside); 21 | return () => { 22 | document.removeEventListener("mousedown", handleClickOutside); 23 | }; 24 | }, [ref]); 25 | } 26 | 27 | return ( 28 |
29 | 30 | 31 | setVisible(true)} 33 | style={{ 34 | position: 'fixed', 35 | bottom: '24px', 36 | right: '24px', 37 | }} 38 | /> 39 |
40 | ) 41 | } 42 | 43 | export default SupportEngine; 44 | 45 | -------------------------------------------------------------------------------- /example/src/Pages/ChatTutorial/ChatFeed/TheirMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function TheirMessage(props) { 4 | const { lastMessage, message } = props 5 | const isFirstMessageByUser = !lastMessage || lastMessage.sender.username !== message.sender.username 6 | 7 | return ( 8 |
9 | { 10 | isFirstMessageByUser && 11 |
15 | } 16 | 17 | { 18 | message.attachments && message.attachments.length > 0 ? 19 | message-attachment : 25 |
33 | { message.text } 34 |
35 | } 36 |
37 | ); 38 | } -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-chat-engine-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start", 8 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build", 9 | "test": "nightwatch -c tests/tests_conf/nightwatch.conf.js", 10 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject", 11 | "deploy": "node ../node_modules/react-scripts/bin/react-scripts.js build && aws s3 cp --recursive ./build s3://support.chatengine.io" 12 | }, 13 | "dependencies": { 14 | "history": "^5.0.0", 15 | "react": "link:../node_modules/react", 16 | "react-chat-engine": "link:..", 17 | "react-dom": "link:../node_modules/react-dom", 18 | "react-grid-system": "^7.1.2", 19 | "react-redux": "^7.2.2", 20 | "react-router-dom": "^5.2.0", 21 | "react-scripts": "link:../node_modules/react-scripts", 22 | "redux": "^4.0.5", 23 | "redux-form": "^8.3.7", 24 | "redux-persist": "^6.0.0", 25 | "redux-promise": "^0.6.0", 26 | "redux-thunk": "^2.3.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3", 30 | "chromedriver": "95.0.0", 31 | "nightwatch": "^1.6.3" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": [ 37 | ">0.2%", 38 | "not dead", 39 | "not ie <= 11", 40 | "not op_mini all" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/components/ChatEngine/components/Effects/Float/useFloat.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSpring } from 'react-spring'; 3 | 4 | export default function useBoop({ 5 | x = 0, 6 | y = 0, 7 | rotation = 0, 8 | scale = 1, 9 | timing = 150, 10 | shadowX = 150, 11 | shadowY = 150, 12 | shadowBlur = 150, 13 | shadowColor = 150, 14 | springConfig = { 15 | tension: 300, 16 | friction: 10, 17 | }, 18 | width = 'auto' 19 | }) { 20 | const [isBooped, setIsBooped] = React.useState(false); 21 | const style = useSpring({ 22 | display: 'inline-block', 23 | backfaceVisibility: 'hidden', 24 | boxShadow: isBooped 25 | ? `${shadowX} ${shadowY} ${shadowBlur} ${shadowColor}` 26 | : '0px 0px 0px red', 27 | transform: isBooped 28 | ? `translate(${x}px, ${y}px) 29 | rotate(${rotation}deg) 30 | scale(${scale})` 31 | : `translate(0px, 0px) 32 | rotate(0deg) 33 | scale(1)`, 34 | config: springConfig, 35 | width: width, 36 | }); 37 | React.useEffect(() => { 38 | if (!isBooped) { return; } 39 | const timeoutId = window.setTimeout(() => { 40 | setIsBooped(false); 41 | }, timing); 42 | return () => { window.clearTimeout(timeoutId); }; 43 | }, [isBooped]); 44 | const trigger = React.useCallback(() => { 45 | console.log('here') 46 | setIsBooped(true); 47 | }, []); 48 | return [style, trigger]; 49 | } -------------------------------------------------------------------------------- /example/tests/tests/testChatSocket.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "tags": ["home", "socket", "feed"], 3 | 4 | 'Try the chat socket feed': function(browser) { 5 | const page = browser.page.homePage(); 6 | 7 | page 8 | .navigate('http://localhost:3001/chat_socket') 9 | .pause(1500) 10 | 11 | // Send a DM 12 | .waitForElementVisible('.ce-message-bubble', 1000, 'Find message bubble') 13 | .waitForElementVisible('@newMessageInput', 1000, 'Find message form') 14 | .waitForElementVisible('@newMessageButton', 1000, 'Find send message button') 15 | .set('@newMessageInput', 'Test-message') 16 | .click('@newMessageButton') 17 | .waitForElementVisible('.ce-message-bubble', 1000, 'Find message bubble'); 18 | }, 19 | 20 | 'Try the user socket feed': function(browser) { 21 | const page = browser.page.homePage(); 22 | 23 | page 24 | .navigate('http://localhost:3001/user_socket_feed') 25 | .pause(1500) 26 | 27 | // Send a DM 28 | .waitForElementVisible('.ce-message-bubble', 1000, 'Find message bubble') 29 | .waitForElementVisible('@newMessageInput', 1000, 'Find message form') 30 | .waitForElementVisible('@newMessageButton', 1000, 'Find send message button') 31 | .set('@newMessageInput', 'Test-message') 32 | .click('@newMessageButton') 33 | .waitForElementVisible('.ce-message-bubble', 1000, 'Find message bubble'); 34 | // TODO: This needs to scroll down 35 | }, 36 | } -------------------------------------------------------------------------------- /example/src/Pages/ChatSocketPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ChatEngineWrapper, ChatSocket, ChatFeed, ChatList } from 'react-chat-engine' 4 | 5 | import { DEVELOPMENT, PROJECT_ID, CHAT_ID, CHAT_ACCESS_KEY } from '../../consts' 6 | 7 | import { Row, Col } from 'react-grid-system' 8 | import { setConfiguration } from 'react-grid-system'; 9 | 10 | setConfiguration({ maxScreenClass: 'xl', gutterWidth: 0 }); 11 | 12 | const HomePage = () => { 13 | const senderUsername = 'Abel Smith' 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | export default HomePage 41 | -------------------------------------------------------------------------------- /example/src/Pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Router, Route, Switch } from 'react-router-dom' 4 | import { bindActionCreators } from 'redux' 5 | 6 | import history from '../Utilities/history' 7 | 8 | import UserSocketFeedPage from './UserSocketFeedPage' 9 | import ChatSocketPage from './ChatSocketPage' 10 | import DirectChatsPage from './DirectChatsPage' 11 | import ChatTutorial from './ChatTutorial' 12 | import SupportEngine from './SupportEngine' 13 | import HomePage from './HomePage' 14 | import GifPage from './GifPage' 15 | 16 | class RootPage extends Component { 17 | render() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | } 34 | 35 | function mapStateToProps(state){ 36 | return { accounts: state.accounts } 37 | } 38 | 39 | function mapDispatchToProps(dispatch){ 40 | return bindActionCreators({}, dispatch) 41 | } 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(RootPage) 44 | -------------------------------------------------------------------------------- /example/src/Pages/SupportEngine/SupportEngine/SupportWindow/ChatEngine.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import { ChatEngineWrapper, Socket, ChatFeed } from 'react-chat-engine' 4 | 5 | const ChatEngine = props => { 6 | const [showChat, setShowChat] = useState(false) 7 | 8 | useEffect(() => { 9 | if (props.visible) { 10 | setTimeout(() => { 11 | setShowChat(true) 12 | }, 500) 13 | } 14 | }) 15 | 16 | return ( 17 |
26 | { 27 | showChat && 28 | 29 | 34 | 35 | 36 | } 37 |
38 | ) 39 | } 40 | 41 | export default ChatEngine; 42 | 43 | const styles = { 44 | chatEngineWindow: { 45 | width: '100%', 46 | backgroundColor: '#fff', 47 | transition: "all 0.5s ease", 48 | WebkitTransition: "all 0.5s ease", 49 | MozTransition: "all 0.5s ease", 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/NewMessageForm/Thumbnail.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | 3 | import { CloseCircleTwoTone } from '@ant-design/icons' 4 | 5 | const Thumbnail = props => { 6 | const [hovered, setHovered] = useState(false) 7 | const [blob, setBlob] = useState('') 8 | 9 | useEffect(() => { 10 | setBlob(URL.createObjectURL(props.file)) 11 | }, [props.file]) 12 | 13 | return ( 14 |
setHovered(true)} 17 | onMouseLeave={() => setHovered(false)} 18 | > 19 | 20 | {props.file 25 | 26 | { 27 | hovered && 28 | props.onRemove && props.onRemove()} 31 | /> 32 | } 33 |
34 | ) 35 | } 36 | 37 | export default Thumbnail 38 | 39 | const styles = { 40 | imageSquare: { 41 | height: '108px', 42 | width: '108px', 43 | border: '1px solid #afafaf', 44 | borderRadius: '8px', 45 | objectFit: 'cover', 46 | display: 'inline', 47 | }, 48 | closeIcon: { 49 | position: 'absolute', 50 | bottom: 'calc(100% - 32px)', 51 | left: '96px', 52 | width: '0px', 53 | cursor: 'pointer' 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Messages/Bubble/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | 3 | import { ChatEngineContext } from '../../../../Context' 4 | 5 | import DatePartition from '../DatePartition' 6 | 7 | import MyMessage from './MyMessage' 8 | import TheirMessage from './TheirMessage' 9 | 10 | const Message = props => { 11 | const { lastMessage, message, nextMessage, chat } = props 12 | 13 | const { conn } = useContext(ChatEngineContext) 14 | 15 | if (!message || !chat) { return
} 16 | 17 | if (!conn || conn === null) { return
} 18 | 19 | return ( 20 |
21 | 26 | 27 | { 28 | message.sender_username === conn.userName || message.sender_username === conn.senderUsername ? 29 | : 37 | 44 | } 45 |
46 | ) 47 | } 48 | 49 | export default Message -------------------------------------------------------------------------------- /example/src/Pages/SupportEngine/SupportEngine/SupportWindow/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import EmailForm from './EmailForm' 4 | import ChatEngine from "./ChatEngine"; 5 | 6 | const SupportWindow = props => { 7 | const [user, setUser] = useState(null) 8 | const [chat, setChat] = useState(null) 9 | 10 | return ( 11 |
15 | setUser(user)} 18 | setChat={chat => setChat(chat)} 19 | /> 20 | 21 | 26 |
27 | ) 28 | } 29 | 30 | export default SupportWindow; 31 | 32 | const styles = { 33 | supportWindow: { 34 | // Position 35 | position: 'fixed', 36 | bottom: '116px', 37 | right: '24px', 38 | // Size 39 | width: '420px', 40 | height: '530px', 41 | maxWidth: 'calc(100% - 48px)', 42 | maxHeight: 'calc(100% - 48px)', 43 | backgroundColor: 'white', 44 | // Border 45 | borderRadius: '12px', 46 | border: `2px solid #7a39e0`, 47 | overflow: 'hidden', 48 | // Transition 49 | transition: "all .5s ease", 50 | WebkitTransition: "all .5s ease", 51 | MozTransition: "all .5s ease", 52 | // Shadow 53 | boxShadow: '0px 0px 16px 6px rgba(0, 0, 0, 0.33)', 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatSettings/SettingsBlock/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { LeftOutlined } from '@ant-design/icons' 4 | 5 | const SettingsBlock = (props) => { 6 | const [collapsed, setCollapsed] = useState(true) 7 | const [hovered, setHovered] = useState(false) 8 | 9 | return ( 10 |
11 |
setHovered(true)} 15 | onMouseLeave={() => setHovered(false)} 16 | onClick={() => setCollapsed(!collapsed)} 17 | style={{ 18 | cursor: 'pointer', 19 | backgroundColor: hovered ? '#f0f0f0' : '#fff', 20 | transition: `background-color 100ms`, 21 | }} 22 | > 23 |
24 | { props.label } 25 |
26 | 27 | 37 |
38 | 39 |
40 | { !collapsed && props.children } 41 |
42 |
43 | ) 44 | } 45 | 46 | export default SettingsBlock 47 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatSettings/PeopleSettings/PersonForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { addPerson, getOtherPeople } from 'react-chat-engine' 5 | 6 | import { AutoCompleteInput } from 'react-chat-engine' 7 | 8 | import PersonOption from './PersonOption' 9 | 10 | const PersonForm = props => { 11 | const [state, setState] = useState({ 12 | value: '', 13 | others: [] 14 | }) 15 | 16 | function handleChange(value) { 17 | setState({ ...state, value }); 18 | } 19 | 20 | function invitePerson(name) { 21 | addPerson( 22 | props.conn, 23 | props.chat.id, 24 | name, 25 | () => { 26 | setState({ ...state, value: '' }); 27 | getOthers(); 28 | } 29 | ) 30 | } 31 | 32 | function getOthers() { 33 | getOtherPeople( 34 | props.conn, 35 | props.chat.id, 36 | (id, others) => setState({ ...state, others }), 37 | () => {}, 38 | ) 39 | } 40 | 41 | function renderOption(option) { 42 | return invitePerson(option.username)} /> 43 | } 44 | 45 | return ( 46 |
47 | getOthers()} 53 | handleChange={(value) => handleChange(value)} 54 | renderOption={(option) => renderOption(option)} 55 | /> 56 |
57 | ) 58 | } 59 | 60 | export default PersonForm 61 | 62 | PersonForm.propTypes = { 63 | chat: PropTypes.object.isRequired, 64 | conn: PropTypes.object.isRequired, 65 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Support 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | - name: Use Node.js ${{ matrix.node-version }} 10 | uses: actions/setup-node@v1 11 | with: 12 | node-version: ${{ matrix.node-version }} 13 | - name: Install & Build Src 14 | run: | 15 | yarn install && yarn build 16 | - name: Create consts.js 17 | run: | 18 | echo "export const DEVELOPMENT = false; export const ROOT_URL = 'https://api.chatengine.io/'; export const PROJECT_ID = '8a1f9edb-a05a-4b55-9d6e-ec399a38f5a9'; export const USER_NAME = ''; export const USER_SECRET = ''; export const CHAT_ID = 0; export const CHAT_ACCESS_KEY = '123';" > example/src/consts.js 19 | - name: Install & Build Example 20 | run: | 21 | cd example && yarn install && yarn build 22 | - name: Deploy to S3 23 | uses: jakejarvis/s3-sync-action@master 24 | with: 25 | args: --acl public-read --delete 26 | env: 27 | AWS_S3_BUCKET: ${{ secrets.AWS_PRODUCTION_BUCKET_NAME }} 28 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 29 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 30 | AWS_REGION: ${{ secrets.AWS_REGION }} 31 | SOURCE_DIR: "example/build" 32 | - name: Invalidate Cloudfront 33 | uses: chetan/invalidate-cloudfront-action@master 34 | env: 35 | DISTRIBUTION: ${{ secrets.AWS_CDN_ID }} 36 | PATHS: "/*" 37 | AWS_REGION: "us-east-1" 38 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_CDN_ACCESS_KEY_ID }} 39 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_CDN_SECRET_ACCESS_KEY }} -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatSettings/PeopleSettings/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | 3 | import { ChatEngineContext } from 'react-chat-engine' 4 | 5 | import PersonRow from './PersonRow' 6 | import PersonForm from './PersonForm' 7 | 8 | import SettingsBlock from '../SettingsBlock' 9 | 10 | const PeopleSettings = () => { 11 | const { conn, chats, activeChat } = useContext(ChatEngineContext) 12 | const chat = chats && chats[activeChat] 13 | 14 | if (!chat || chat.is_direct_chat) { return
} 15 | 16 | function renderChatPeople(people, chat) { 17 | return people.map((chatPerson, index) => { 18 | return ( 19 | 25 | ) 26 | }) 27 | } 28 | 29 | return ( 30 |
33 | 37 |
38 |
39 | 40 | { renderChatPeople(chat.people, chat) } 41 | 42 |
43 | 44 | { 45 | conn && chat && conn.userName === chat.admin.username && 46 | 47 | } 48 |
49 | 50 |
51 | ) 52 | } 53 | 54 | export default PeopleSettings 55 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/MessageFormSocial/ImagesInput.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { PaperClipOutlined } from '@ant-design/icons' 4 | 5 | const ImagesInput = props => { 6 | const [state, setState] = useState({ 7 | hovered: false, 8 | }) 9 | 10 | function onSelect(event) { 11 | const files = Array.from(event.target.files) 12 | props.onSelectFiles && props.onSelectFiles(files) 13 | } 14 | 15 | return ( 16 |
22 | 36 | 37 | onSelect(e)} 44 | onClick={(e) => e.target.value = null} 45 | /> 46 |
47 | ); 48 | } 49 | 50 | export default ImagesInput 51 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 18 | 27 | Support | Chat Engine 28 | 29 | 30 | 31 | 34 | 35 |
36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Messages/Bubble/FileView.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { FileOutlined } from '@ant-design/icons' 4 | 5 | import { getFileName } from './file' 6 | 7 | import { LoadingOutlined } from '@ant-design/icons' 8 | 9 | const FileView = props => { 10 | const [hovered, setHovered] = useState(false) 11 | const { attachment } = props 12 | const style={ 13 | ...styles.fileView, 14 | ...{ 15 | color: hovered ? '#1890ff' : '#434343', 16 | border: hovered ? '1px solid #1890ff' : '1px solid #434343', 17 | } 18 | } 19 | 20 | if (!attachment) { 21 | return ( 22 |
23 | 24 |
25 | ) 26 | } 27 | 28 | return ( 29 |
setHovered(true)} 32 | onMouseLeave={() => setHovered(false)} 33 | onClick={() => window.open(attachment.file)} 34 | > 35 | {' '}{ getFileName(attachment.file) } 36 |
37 | ) 38 | } 39 | 40 | export default FileView 41 | 42 | const styles = { 43 | loadingContainer: { 44 | display: 'inline-block', 45 | borderRadius: '14px', 46 | marginRight: '2px', 47 | height: '48px', 48 | width: '136px', 49 | marginBottom: '4px', 50 | marginLeft: '4px', 51 | backgroundColor: '#d9d9d9', 52 | }, 53 | fileView: { 54 | padding: '12px', 55 | borderRadius: '14px', 56 | display: 'inline-block', 57 | marginBottom: '4px', 58 | marginRight: '2px', 59 | cursor: 'pointer', 60 | } 61 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/IsTyping/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState, useEffect } from 'react' 2 | 3 | import { ChatEngineContext } from '../../../Context' 4 | 5 | import { stringToColor } from '../../Utilities/colorMapping' 6 | 7 | const IsTyping = () => { 8 | const didMountRef = useRef(false) 9 | const [currentTime, setCurrentTime] = useState(Date.now()) 10 | const { conn, activeChat, typingCounter } = useContext(ChatEngineContext) 11 | const typers = typingCounter && typingCounter[activeChat] ? typingCounter[activeChat] : [] 12 | 13 | useEffect(() => { 14 | if (!didMountRef.current) { 15 | didMountRef.current = true 16 | setInterval(() => { 17 | setCurrentTime(Date.now()) 18 | }, 1000) // Check time every second 19 | } 20 | }) 21 | 22 | if (!conn || conn === null) return
23 | 24 | return ( 25 |
26 | { 27 | Object.keys(typers).map((username, index) => { 28 | if (conn.userName !== username && currentTime < typers[username] + 2000) { 29 | return ( 30 |
35 | {`${username} is typing...`} 36 |
37 | ) 38 | 39 | } else { 40 | return
41 | } 42 | }) 43 | } 44 |
45 | ) 46 | } 47 | 48 | export default IsTyping -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/MessageFormSocial/MessageInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export default class MessageInput extends Component { 4 | state = { 5 | value: null, 6 | height: 0 7 | } 8 | 9 | resize() { 10 | var textarea = document.getElementById("msg-textarea"); 11 | textarea.style.height = ""; 12 | textarea.style.height = Math.min(textarea.scrollHeight, 150) + "px"; 13 | this.setState({ height: Math.min(textarea.scrollHeight, 150) }) 14 | } 15 | 16 | componentDidMount() { this.resize() } 17 | 18 | handleChange(e) { 19 | this.resize() 20 | this.props.handleChange && this.props.handleChange(e) 21 | } 22 | 23 | onKeyDown(e) { 24 | if (e.key === 'Enter') { 25 | e.preventDefault() 26 | 27 | if (this.props.value.length > 0) { 28 | this.props.onSubmit && this.props.onSubmit(e) 29 | } 30 | } 31 | } 32 | 33 | render() { 34 | return ( 35 | this.handleChange(e)} 42 | onKeyDown={(e) => this.onKeyDown(e)} 43 | /> 44 | ) 45 | } 46 | } 47 | 48 | const styles = { 49 | input: { 50 | border: '1px solid white', 51 | width: 'calc(100% - 100px)', 52 | outline: 'none', 53 | fontSize: '15px', 54 | fontFamily: 'Avenir', 55 | paddingLeft: '12px', 56 | paddingRight: '12px', 57 | position: 'relative', 58 | left: '12px', 59 | resize: 'none', 60 | overflowX: 'hidden' 61 | } 62 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/NewMessageForm/AttachmentsInput.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { PaperClipOutlined } from '@ant-design/icons' 4 | 5 | const AttachmentsInput = props => { 6 | const [state, setState] = useState({ 7 | hovered: false, 8 | }) 9 | 10 | function onSelect(event) { 11 | const files = Array.from(event.target.files) 12 | props.onSelectFiles && props.onSelectFiles(files) 13 | } 14 | 15 | return ( 16 |
21 | 35 | 36 | onSelect(e)} 42 | onClick={(e) => e.target.value = null} 43 | /> 44 |
45 | ); 46 | } 47 | 48 | const styles = { 49 | icon: { 50 | background: 'none', 51 | border: 'none', 52 | cursor: 'pointer', 53 | display: 'inline-block', 54 | float: 'left', 55 | height: '24px', 56 | padding: '4px 5px', 57 | width: '28px', 58 | } 59 | } 60 | 61 | export default AttachmentsInput 62 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/ChatHeader/ChatListDrawer.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react' 2 | 3 | import { MenuOutlined } from '@ant-design/icons' 4 | 5 | import { ChatEngineContext } from '../../../Context' 6 | 7 | import ChatList from '../../ChatList' 8 | 9 | const ChatListDrawer = props => { 10 | const [isOpen, setIsOpen] = useState(false) 11 | const context = useContext(ChatEngineContext) 12 | const allProps = {...props, ...context.conn} 13 | 14 | return ( 15 |
16 | setIsOpen(true)} 18 | style={{ color: 'rgb(24, 144, 255)', outline: 'none' }} 19 | /> 20 | 21 | { 22 | isOpen && 23 |
24 | { 25 | context.conn !== null && context.conn.renderChatList ? 26 | context.conn.renderChatList(context) : 27 | setIsOpen(false)} 30 | onChatClick={() => setIsOpen(false)} 31 | /> 32 | } 33 |
34 | } 35 |
36 | ) 37 | } 38 | 39 | export default ChatListDrawer 40 | 41 | const styles = { 42 | drawerContainer: { 43 | position: 'fixed', 44 | zIndex: '1', 45 | top: '0px', 46 | left: '0px', 47 | width: '100%', 48 | height: '100%', 49 | backgroundColor: 'white', 50 | textAlign: 'left' 51 | }, 52 | titleContainer: { 53 | width: '100%', 54 | padding: '24px 0px', 55 | textAlign: 'center', 56 | color: 'rgb(24, 144, 255)', 57 | }, 58 | titleText: { 59 | fontSize: '24px', 60 | fontWeight: '600', 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatSettings/ChatSettingsTop/TitleForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import PropTypes from 'prop-types' 3 | 4 | import { editChat, TextInput } from 'react-chat-engine' 5 | 6 | const NewMessageForm = props => { 7 | const didMountRef = useRef(false) 8 | const [state, setState] = useState({ 9 | activeChat: null, 10 | value: '' 11 | }) 12 | 13 | function handleChange(event) { 14 | setState({ ...state, value: event.target.value }); 15 | } 16 | 17 | function handleSubmit(event) { 18 | event.preventDefault(); 19 | 20 | editChat( 21 | props.conn, 22 | props.chat.id, 23 | {title: state.value}, 24 | (data) => {} 25 | ) 26 | } 27 | 28 | useEffect(() => { 29 | if (!didMountRef.current) { 30 | didMountRef.current = true 31 | 32 | } else { 33 | if(state.activeChat !== props.chat.id) { 34 | setState({ 35 | ...state, 36 | value: props.chat.title, 37 | activeChat: props.chat.id 38 | }) 39 | } 40 | } 41 | }) 42 | 43 | return ( 44 |
handleSubmit(e)} className='ce-chat-title-form'> 45 | handleChange(e)} 50 | style={{ 51 | fontSize: '16px', 52 | fontWeight: '600', 53 | textAlign: 'center', 54 | border: '0px solid white', 55 | width: '100%', 56 | }} 57 | /> 58 | 59 | ); 60 | } 61 | 62 | export default NewMessageForm 63 | 64 | NewMessageForm.propTypes = { 65 | chat: PropTypes.object.isRequired, 66 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/Messages/Bubble/Thumbnail.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { LoadingOutlined } from '@ant-design/icons' 4 | 5 | const Thumbnail = props => { 6 | const [hovered, setHovered] = useState(false) 7 | const { attachment } = props 8 | const style={ 9 | ...styles.thumbnail, 10 | ...{ border: hovered ? '1px solid #1890ff' : '1px solid #fff' } 11 | } 12 | 13 | if (!attachment) { 14 | return ( 15 |
16 | 17 |
18 | ) 19 | } 20 | 21 | return ( 22 | window.open(attachment.file)} 24 | onMouseEnter={() => setHovered(true)} 25 | onMouseLeave={() => setHovered(false)} 26 | src={attachment.file} 27 | alt={'thumb-nail'} 28 | style={style} 29 | /> 30 | ) 31 | } 32 | 33 | export default Thumbnail 34 | 35 | const styles = { 36 | loadingContainer: { 37 | width: '100%', 38 | cursor: 'pointer', 39 | textAlign: 'right', 40 | display: 'inline-block', 41 | objectFit: 'cover', 42 | borderRadius: '0.3em', 43 | marginRight: '2px', 44 | marginBottom: '4px', 45 | height: '30vw', 46 | width: '30vw', 47 | maxHeight: '200px', 48 | maxWidth: '200px', 49 | minHeight: '100px', 50 | minWidth: '100px', 51 | backgroundColor: '#d9d9d9', 52 | }, 53 | thumbnail: { 54 | width: '100%', 55 | cursor: 'pointer', 56 | textAlign: 'right', 57 | display: 'inline', 58 | objectFit: 'cover', 59 | borderRadius: '0.3em', 60 | paddingRight: '2px', 61 | 62 | height: '30vw', 63 | width: '30vw', 64 | maxHeight: '200px', 65 | maxWidth: '200px', 66 | minHeight: '100px', 67 | minWidth: '100px', 68 | } 69 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/components/Avatar/Dot/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { stringToColor } from '../../../Utilities/colorMapping' 5 | 6 | 7 | export default class Dot extends Component { 8 | state = { avatar: '' } 9 | 10 | updateImg() { 11 | let { avatar } = this.props 12 | avatar = avatar && avatar !== null ? avatar : '' 13 | 14 | if (avatar.split('?')[0] !== this.state.avatar.split('?')[0]) { 15 | this.setState({ avatar }) 16 | } 17 | } 18 | 19 | componentDidMount() { this.updateImg() } 20 | 21 | componentDidUpdate() { this.updateImg() } 22 | 23 | render() { 24 | const { username } = this.props 25 | const color = stringToColor(username) 26 | const customStyle = this.props.style ? this.props.style : {} 27 | 28 | return ( 29 |
46 | ) 47 | } 48 | } 49 | 50 | const styles = { 51 | dot: { 52 | borderRadius: '13px', 53 | textAlign: 'center', 54 | // CSS Transitions 55 | transition: "all .33s ease", 56 | WebkitTransition: "all .33s ease", 57 | MozTransition: "all .33s ease", 58 | } 59 | } 60 | 61 | Dot.propTypes = { 62 | avatar: PropTypes.string, 63 | username: PropTypes.string, 64 | style: PropTypes.object, 65 | } -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/ScrollDownBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect, useRef } from 'react' 2 | 3 | import { ChatEngineContext } from 'react-chat-engine' 4 | 5 | import { CaretDownOutlined } from '@ant-design/icons' 6 | 7 | import { animateScroll } from "react-scroll" 8 | 9 | const ScrollDownBar = (props) => { 10 | const didMountRef = useRef(false) 11 | const [isVisible, setIsVisible] = useState(false) 12 | const { conn, isBottomVisible } = useContext(ChatEngineContext) 13 | const { chat } = props 14 | 15 | useEffect(() => { 16 | if (!didMountRef.current) { 17 | didMountRef.current = true 18 | setTimeout( 19 | () => setIsVisible(true), 20 | props.renderDelay ? props.renderDelay : 0 21 | ) 22 | } 23 | }) 24 | 25 | if (conn === null || !chat || chat === null) return
26 | 27 | let lastReadMessage = undefined 28 | chat.people.map(person => { 29 | if (person.person.username === conn.userName) { 30 | lastReadMessage = person.last_read 31 | } 32 | }) 33 | 34 | if ( 35 | !isVisible || 36 | isBottomVisible || 37 | chat.last_message.id === undefined || 38 | chat.last_message.id === lastReadMessage 39 | ) { return
} 40 | 41 | return ( 42 |
animateScroll.scrollToBottom({ duration: 333, containerId: "ce-feed-container" })} 57 | > 58 | 59 | {conn.userName ? ' Unread Messages' : ' Scroll to Bottom'} 60 |
61 | ) 62 | } 63 | 64 | export default ScrollDownBar 65 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatSettings/index.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | 3 | import { ChatEngineContext } from 'react-chat-engine' 4 | 5 | import PeopleSettings from './PeopleSettings' 6 | import PhotosSettings from './PhotosSettings' 7 | import OptionsSettings from './OptionsSettings' 8 | import ChatSettingsTop from './ChatSettingsTop' 9 | 10 | const ChatSettings = props => { 11 | const { conn, chats, activeChat } = useContext(ChatEngineContext) 12 | const chat = chats && chats[activeChat] 13 | 14 | if (conn === null) return
15 | 16 | return ( 17 |
18 |
19 | { 20 | props.renderChatSettingsTop ? 21 | props.renderChatSettingsTop(conn, chat) : 22 | 23 | } 24 | 25 | { 26 | props.renderPeopleSettings ? 27 | props.renderPeopleSettings(conn, chat) : 28 | 29 | } 30 | 31 | { 32 | props.renderPhotosSettings ? 33 | props.renderPhotosSettings(chat) : 34 | 35 | } 36 | 37 | { 38 | conn && chat && conn.userName === chat.admin.username && 39 |
40 | { 41 | props.renderOptionsSettings ? 42 | props.renderOptionsSettings(conn, chat) : 43 | 44 | } 45 |
46 | } 47 |
48 |
49 | ) 50 | } 51 | 52 | export default ChatSettings 53 | 54 | const styles = { 55 | settingsContainer: { 56 | height: '100%', 57 | overflow: 'scroll', 58 | overflowX: 'hidden', 59 | borderLeft: '1px solid #afafaf', 60 | backgroundColor: 'white', 61 | fontFamily: 'Avenir' 62 | } 63 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-chat-engine", 3 | "version": "1.11.28", 4 | "description": "Cheap and easy chat API", 5 | "author": "alamorre", 6 | "license": "MIT", 7 | "repository": "alamorre/react-chat-engine", 8 | "main": "dist/index.js", 9 | "module": "dist/index.modern.js", 10 | "source": "src/index.js", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "build": "microbundle-crl --no-compress --format modern,cjs", 16 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 17 | "prepare": "run-s build", 18 | "test": "nightwatch -c tests/tests_conf/nightwatch.conf.js", 19 | "test-headless": "nightwatch -c tests/tests_conf/nightwatch.conf.js --headless", 20 | "predeploy": "cd example && yarn install && yarn run build", 21 | "deploy": "gh-pages -d example/build" 22 | }, 23 | "peerDependencies": { 24 | "react": "^16.8.0 || ^17.0.1 || ^18.0.0" 25 | }, 26 | "devDependencies": { 27 | "babel-eslint": "^10.0.3", 28 | "cross-env": "^7.0.2", 29 | "eslint": "^6.8.0", 30 | "eslint-config-prettier": "^6.7.0", 31 | "eslint-config-standard": "^14.1.0", 32 | "eslint-config-standard-react": "^9.2.0", 33 | "eslint-plugin-import": "^2.18.2", 34 | "eslint-plugin-node": "^11.0.0", 35 | "eslint-plugin-prettier": "^3.1.1", 36 | "eslint-plugin-promise": "^4.2.1", 37 | "eslint-plugin-react": "^7.17.0", 38 | "eslint-plugin-standard": "^4.0.1", 39 | "gh-pages": "^2.2.0", 40 | "microbundle-crl": "^0.13.10", 41 | "nightwatch": "^1.5.1", 42 | "npm-run-all": "^4.1.5", 43 | "prettier": "^2.0.4", 44 | "prop-types": "^15.7.2", 45 | "react": "^16.13.1", 46 | "react-dom": "^16.13.1", 47 | "react-scripts": "^3.4.1" 48 | }, 49 | "files": [ 50 | "dist" 51 | ], 52 | "dependencies": { 53 | "@ant-design/icons": "^4.2.2", 54 | "axios": "0.21.3", 55 | "html-to-text": "^7.1.1", 56 | "lodash": "4.17.21", 57 | "nextjs-websocket": "^1.0.7", 58 | "react-grid-system": "^7.1.1", 59 | "react-quill": "^1.3.5", 60 | "react-scroll": "^1.8.1", 61 | "react-spring": "^9.2.2", 62 | "styled-jsx": "^4.0.1", 63 | "websocket": "^1.0.33" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/ChatEngine/ChatFeed/NewMessageForm/MessageInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export default class MessageInput extends Component { 4 | state = { 5 | focused: false, 6 | value: null, 7 | height: 0 8 | } 9 | 10 | resize() { 11 | var textarea = document.getElementById("msg-textarea"); 12 | textarea.style.height = ""; 13 | textarea.style.height = Math.min(textarea.scrollHeight, 150) + "px"; 14 | this.setState({ height: Math.min(textarea.scrollHeight, 150) }) 15 | } 16 | 17 | componentDidMount() { this.resize() } 18 | 19 | handleChange(e) { 20 | this.resize() 21 | this.props.handleChange && this.props.handleChange(e) 22 | } 23 | 24 | onKeyDown(e) { 25 | if (e.key === 'Enter') { 26 | e.preventDefault() 27 | 28 | if (this.props.value.length > 0) { 29 | this.props.onSubmit && this.props.onSubmit(e) 30 | } 31 | } 32 | } 33 | 34 | render() { 35 | const style = { 36 | ...styles.input, 37 | ...{ overflowY: this.state.height === 150 ? 'scroll' : 'hidden' } 38 | } 39 | 40 | return ( 41 |