8 |
14 | Hey it's Adam 🤙
15 |
16 |
17 |
setHovered(true)}
19 | onMouseLeave={() => setHovered(false)}
20 | onClick={() => props.onClick()}
21 | style={{
22 | ...styles.chatWithMeButton,
23 | ...{ border: hovered ? '1px solid #f9f0ff' : '4px solid #7a39e0' }
24 | }}
25 | />
26 |
27 | )
28 | }
29 |
30 | export default Avatar;
31 |
32 | const styles = {
33 | chatWithMeButton: {
34 | cursor: 'pointer',
35 | boxShadow: '0px 0px 16px 6px rgba(0, 0, 0, 0.33)',
36 | // Border
37 | borderRadius: '50%',
38 | // Transition
39 | transition: "all .33s ease",
40 | WebkitTransition: "all .33s ease",
41 | MozTransition: "all .33s ease",
42 | // Background
43 | backgroundImage: `url(https://chat-engine-assets.s3.amazonaws.com/tutorials/my-face-min.png)`,
44 | backgroundRepeat: 'no-repeat',
45 | backgroundPosition: 'center',
46 | backgroundSize: '84px',
47 | // Size
48 | width: '84px',
49 | height: '84px',
50 | },
51 | avatarHello: {
52 | position: 'absolute',
53 | left: 'calc(-100% - 44px - 28px)',
54 | top: 'calc(50% - 24px)',
55 | // Layering
56 | zIndex: '10000',
57 | boxShadow: '0px 0px 16px 6px rgba(0, 0, 0, 0.33)',
58 | // Border + Color
59 | padding: '12px 12px 12px 16px',
60 | borderRadius: '24px',
61 | backgroundColor: '#f9f0ff',
62 | color: 'black',
63 | // Transition
64 | transition: "all .33s ease",
65 | WebkitTransition: "all .33s ease",
66 | MozTransition: "all .33s ease",
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/example/src/Pages/ChatTutorial/ChatFeed/MessageForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { SendOutlined, PictureOutlined } from '@ant-design/icons'
4 |
5 | import { sendMessage, isTyping } from 'react-chat-engine'
6 |
7 | export default class MessageForm extends React.Component {
8 | state = { value: '' }
9 |
10 | handleChange(event) {
11 | this.setState({ value: event.target.value });
12 | isTyping(this.props, this.props.chatId)
13 | }
14 |
15 | handleSubmit(event) {
16 | event.preventDefault();
17 | const text = this.state.value.trim()
18 | if (text.length > 0 ) {
19 | sendMessage(
20 | this.props.creds,
21 | this.props.chatId,
22 | { text }
23 | )
24 | }
25 | this.setState({ value: '' })
26 | }
27 |
28 | handleUpload(event) {
29 | sendMessage(
30 | this.props.creds,
31 | this.props.chatId,
32 | { files: event.target.files, text: '' }
33 | )
34 | }
35 |
36 | render() {
37 | return (
38 |
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatFeed/ChatHeader/ChatSettingsDrawer.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react'
2 |
3 | import { SettingOutlined, CloseOutlined } from '@ant-design/icons'
4 |
5 | import { ChatEngineContext } from 'react-chat-engine'
6 |
7 | import ChatSettings from '../../ChatSettings'
8 |
9 | const ChatSettingsDrawer = 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 |
setIsOpen(false)}
27 | />
28 |
29 |
30 |
31 | Chat Settings
32 |
33 |
34 |
35 | {
36 | context.conn !== null && context.conn.renderChatSettings ?
37 | context.conn.renderChatSettings(context) :
38 |
39 | }
40 |
41 | }
42 |
43 | );
44 | }
45 |
46 | export default ChatSettingsDrawer
47 |
48 | const styles = {
49 | drawerContainer: {
50 | position: 'fixed',
51 | zIndex: '1',
52 | top: '0px',
53 | left: '0px',
54 | width: '100%',
55 | height: '100%',
56 | backgroundColor: 'white',
57 | textAlign: 'left'
58 | },
59 | closeIcon: {
60 | position: 'absolute',
61 | left: '28px',
62 | top: '32px'
63 | },
64 | titleContainer: {
65 | width: '100%',
66 | padding: '24px 0px',
67 | textAlign: 'center',
68 | color: 'rgb(24, 144, 255)',
69 | },
70 | titleText: {
71 | fontSize: '24px',
72 | fontWeight: '600',
73 | },
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/Sockets/Socket/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useContext, useEffect } from 'react'
2 |
3 | import SocketChild from './Socket4'
4 | import { getOrCreateSession } from './getOrCreateSession'
5 | import { ChatEngineContext } from 'react-chat-engine'
6 |
7 | const Socket = (props) => {
8 | const didMountRef = useRef(false)
9 | const [isHidden, setIsHidden] = useState(false)
10 | const { setConn, setCreds, setSessionToken } = useContext(ChatEngineContext)
11 |
12 | useEffect(() => {
13 | if (!didMountRef.current) {
14 | didMountRef.current = true
15 | getSession()
16 | }
17 | })
18 |
19 | function getSession() {
20 | const sessionKey = `${props.projectID}/${props.userName}/${props.userSecret}`
21 |
22 | if (localStorage.getItem(sessionKey) !== null) {
23 | setConn(props)
24 | setCreds(props)
25 | setSessionToken(localStorage.getItem(sessionKey))
26 | console.log('Local fetch!')
27 | return
28 | }
29 |
30 | getOrCreateSession(
31 | props,
32 | (data) => {
33 | setConn(props)
34 | setCreds(props)
35 | setSessionToken(data.token)
36 | localStorage.setItem(sessionKey, data.token)
37 | },
38 | (e) => {
39 | if (e.response && e.response.status === 403) {
40 | console.log(
41 | `Your login credentials were not correct: \n
42 | Project ID: ${props.projectID} \n
43 | Username: ${props.userName} \n
44 | User Secret: ${props.userSecret}\n
45 | Double check these credentials to make sure they're correct.\n
46 | If all three are correct, try resetting the username and secret in the Online Dashboard or Private API.`
47 | )
48 | setConn(undefined)
49 | setCreds(undefined)
50 | localStorage.removeItem(sessionKey)
51 | props.onFailAuth && props.onFailAuth(props)
52 | }
53 | setTimeout(() => getSession(), 10 * 1000)
54 | }
55 | )
56 | }
57 |
58 | function reRender() {
59 | setIsHidden(true)
60 | setTimeout(() => setIsHidden(false), 100)
61 | }
62 |
63 | if (isHidden) return
64 |
65 | return
reRender()} />
66 | }
67 |
68 | export default Socket
69 |
--------------------------------------------------------------------------------
/example/tests/tests/testDirectChats.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "tags": ["home", "direct", 'dms'],
3 |
4 | 'Try the DMs': function(browser) {
5 | const page = browser.page.homePage();
6 |
7 | page
8 | .navigate('http://localhost:3001/direct')
9 | .pause(1500)
10 |
11 | // New DM
12 | .waitForElementVisible('@newDirectChatInput', 1000, 'Find user name input')
13 | .waitForElementVisible('@newDirectChatButton', 1000, 'Find new DM button')
14 | .set('@newDirectChatInput', 'Matthew_Orr')
15 | .click('@newDirectChatButton')
16 | .pause(2000)
17 |
18 | // New DM but only one exists
19 | .waitForElementVisible('@newDirectChatInput', 1000, 'Find user name input')
20 | .waitForElementVisible('@newDirectChatButton', 1000, 'Find new DM button')
21 | .set('@newDirectChatInput', 'Matthew_Orr')
22 | .click('@newDirectChatButton')
23 | .pause(2000)
24 |
25 | // Look for new, empty DM Chat
26 | .waitForElementVisible('#ce-chat-card-title-Matthew_Orr', 1000, 'Find new DM chat card')
27 | .waitForElementVisible('#ce-chat-feed-title-Matthew_Orr', 1000, 'Find new DM title')
28 |
29 | // Assert no add person
30 | .waitForElementNotPresent('@addUserInput', 1000, 'Find no add user input')
31 |
32 | // Send a DM
33 | .waitForElementVisible('@newMessageInput', 1000, 'Find message form')
34 | .waitForElementVisible('@newMessageButton', 1000, 'Find send message button')
35 | .set('@newMessageInput', 'Test-message')
36 | .click('@newMessageButton')
37 | .waitForElementVisible('.ce-message-bubble', 1000, 'Find message bubble')
38 |
39 | // Delete DM Chat
40 | .getLocationInView('@optionsSettings')
41 | .waitForElementVisible('@optionsSettings', 1000, 'Wait for options drop down')
42 | .click('@optionsSettings')
43 | .getLocationInView('@deleteChatButton')
44 | .waitForElementVisible('@deleteChatButton', 1000, 'Wait for delete chat button')
45 | .click('@deleteChatButton')
46 |
47 | // Look for new, empty DM Chat
48 | .waitForElementNotPresent('#ce-chat-card-title-Matthew_Orr', 2000, 'Find no more DM chat card')
49 | .waitForElementNotPresent('#ce-chat-feed-title-Matthew_Orr', 2000, 'Find no more DM title');
50 | },
51 | }
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatList/ChatCard/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ChatCard = () => {
4 | return (
5 |
43 | )
44 | }
45 |
46 | const styles={
47 | chatContainer: {
48 | padding: '16px',
49 | paddingBottom: '12px',
50 | cursor: 'pointer'
51 | },
52 | titleText: {
53 | fontWeight: '500',
54 | paddingBottom: '4px',
55 | whiteSpace: 'nowrap',
56 | overflow: 'hidden'
57 | },
58 | messageText: {
59 | width: '75%',
60 | color: 'rgba(153, 153, 153, 1)',
61 | fontSize: '14px',
62 | whiteSpace: 'nowrap',
63 | overflow: 'hidden',
64 | display: 'inline-block'
65 | },
66 | loadingBar: {
67 | borderRadius: '4px',
68 | backgroundColor: '#e2e2e2',
69 | height: '12px',
70 | display: 'inline-block'
71 | }
72 | }
73 |
74 | export default ChatCard;
75 |
--------------------------------------------------------------------------------
/src/components/ChatEngine/components/Button/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { PlusOutlined, DeleteOutlined, UserAddOutlined } from '@ant-design/icons'
5 |
6 | export default class Button extends Component {
7 | state = {
8 | hovered: false
9 | }
10 |
11 | render() {
12 | const { value, icon, theme, style, id, type, onClick } = this.props
13 |
14 | const customStyle = style ? style : {}
15 | const hoverStyle = this.state.hovered ? styles.hoverButton : {}
16 | const themeStyle = theme === 'danger' ? styles.dangerButton : styles.button
17 |
18 | return (
19 |
34 | )
35 | }
36 | }
37 |
38 | const styles = {
39 | button: {
40 | color: 'white',
41 | border: 'none',
42 | outline: 'none',
43 | height: '36px',
44 | fontSize: '15px',
45 | cursor: 'pointer',
46 | padding: '8px 16px',
47 | borderRadius: '33px',
48 | backgroundColor: '#1890ff'
49 | },
50 | dangerButton: {
51 | color: 'red',
52 | border: 'none',
53 | outline: 'none',
54 | height: '36px',
55 | fontSize: '15px',
56 | cursor: 'pointer',
57 | padding: '8px 16px',
58 | borderRadius: '33px',
59 | backgroundColor: 'white',
60 | border: '1px solid red',
61 | },
62 | hoverButton: {
63 | opacity: '0.66',
64 | }
65 | }
66 |
67 | Button.propTypes = {
68 | value: PropTypes.string,
69 | onClick: PropTypes.func,
70 | style: PropTypes.object,
71 | id: PropTypes.string,
72 | icon: PropTypes.oneOf([undefined, 'plus', 'delete', 'user-add']),
73 | theme: PropTypes.oneOf([undefined, 'danger']),
74 | type: PropTypes.oneOf([undefined, 'submit']),
75 | }
--------------------------------------------------------------------------------
/src/components/ChatEngine/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 |
3 | import { ChatEngineContext } from '../Context'
4 |
5 | import _ from 'lodash'
6 |
7 | import { Socket } from '../Sockets'
8 |
9 | import ChatList from './ChatList'
10 | import ChatFeed from './ChatFeed'
11 | import ChatSettings from './ChatSettings'
12 |
13 | import { Row, Col } from 'react-grid-system'
14 | import { setConfiguration } from 'react-grid-system'
15 |
16 | // TODO: One off imports for Teams tutorial
17 | import { getLatestMessages } from 'react-chat-engine'
18 |
19 | setConfiguration({ maxScreenClass: 'xl', gutterWidth: 0 })
20 |
21 | const ChatEngine = (props) => {
22 | const context = useContext(ChatEngineContext)
23 | const { height } = props
24 | const propsAndContext = { ...props, ...context }
25 |
26 | return (
27 |
31 |
32 |
33 |
34 |
40 | {props.renderChatList ? (
41 | props.renderChatList(propsAndContext)
42 | ) : (
43 | {
47 | if (props.renderChatFeed) {
48 | getLatestMessages(props, chatID, 45, (id, list) => {
49 | context.setMessages({ ..._.mapKeys(list, 'created') })
50 | })
51 | }
52 | }}
53 | />
54 | )}
55 |
56 |
57 |
63 | {props.renderChatFeed ? (
64 | props.renderChatFeed(propsAndContext)
65 | ) : (
66 |
67 | )}
68 |
69 |
70 |
76 | {props.renderChatSettings ? (
77 | props.renderChatSettings(props)
78 | ) : (
79 |
80 | )}
81 |
82 |
83 |
84 | )
85 | }
86 |
87 | export default ChatEngine
88 |
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatSettings/PeopleSettings/PersonRow/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { removePerson } from 'react-chat-engine'
5 |
6 | import { Avatar, Button } from 'react-chat-engine'
7 |
8 | const PersonRow = props => {
9 | const [selected, setSelected] = useState(false)
10 |
11 | const { person, chat, conn } = props
12 |
13 | if (!person || !chat) { return }
14 |
15 | if (!conn || conn === null) return
16 |
17 | function onRemovePerson() {
18 | removePerson(
19 | props.conn,
20 | props.chat.id,
21 | props.person.username
22 | )
23 | }
24 |
25 | function renderPersonText(person) {
26 | if (person.first_name && person.first_name !== null) {
27 | return `${person.first_name}${person.last_name ? ` ${person.last_name}` : ''}`
28 | } else {
29 | return person.username
30 | }
31 | }
32 |
33 | return (
34 | setSelected(true)}
37 | onMouseLeave={() => setSelected(false)}
38 | >
39 |
49 |
50 |
54 | { renderPersonText(person) }
55 |
56 |
57 | {
58 | selected && (conn.userName === chat.admin.username) && (person.username !== chat.admin.username) &&
59 |
63 |
69 | }
70 |
71 | )
72 | }
73 |
74 | export default PersonRow
75 |
76 | PersonRow.propTypes = {
77 | person: PropTypes.object.isRequired,
78 | chat: PropTypes.object.isRequired,
79 | conn: PropTypes.object,
80 | }
--------------------------------------------------------------------------------
/example/src/Pages/GifPage/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 |
5 | import { login, logout } from '../../Actions/Accounts'
6 |
7 | import { ChatEngine } from 'react-chat-engine'
8 |
9 | const ROOT_URL = 'https://api.chatengine.io/'
10 | const PROJECT_ID = '12b6b495-2c5d-43d6-a210-b5c26f1e0126'
11 |
12 | const HomePage = props => {
13 | const [loading, setLoading] = useState(false)
14 | const [userName, setUserName] = useState('')
15 | const [userSecret, setUserSecret] = useState('')
16 |
17 | function submit(){
18 | setLoading(true)
19 |
20 | props.login(
21 | { rootUrl: ROOT_URL, projectID: PROJECT_ID, userName, userSecret },
22 | () => setLoading(false),
23 | (error) => console.log(error)
24 | )
25 | }
26 |
27 | if (!props.accounts.userName) {
28 | return (
29 |
30 | setUserName(e.target.value)}
35 | />
36 |
37 | setUserSecret(e.target.value)}
42 | />
43 |
44 |
51 |
52 | )
53 | }
54 |
55 | return (
56 |
57 |
63 |
64 |
70 |
71 | )
72 | }
73 |
74 | function mapStateToProps(state){
75 | return { accounts: state.accounts }
76 | }
77 |
78 | function mapDispatchToProps(dispatch){
79 | return bindActionCreators({ login, logout }, dispatch)
80 | }
81 |
82 | export default connect(mapStateToProps, mapDispatchToProps)(HomePage)
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatList/NewChatForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react'
2 |
3 | import { CloseOutlined } from '@ant-design/icons'
4 |
5 | import { ChatEngineContext } from '../../../Context'
6 |
7 | import { newChat, Button, TextInput } from 'react-chat-engine'
8 |
9 | const NewChatForm = (props) => {
10 | const { conn } = useContext(ChatEngineContext)
11 | const [value, setValue] = useState('')
12 | const [selected, setSelected] = useState(false)
13 |
14 | function handleChange(event) {
15 | setValue(event.target.value)
16 | }
17 |
18 | function handleSubmit(event) {
19 | event.preventDefault()
20 |
21 | if (conn && value.trim().length > 0) {
22 | newChat(
23 | conn,
24 | { title: value },
25 | () => setSelected(false)
26 | )
27 | }
28 |
29 | setValue('')
30 | }
31 |
32 | return (
33 |
34 | {
35 | props.onClose &&
36 |
37 | props.onClose()}
40 | />
41 |
42 | }
43 |
44 |
51 | {
52 | selected ?
53 |
:
64 |
65 |
66 |
67 | My Chats
68 |
69 |
70 |
71 |
72 |
78 |
79 | }
80 |
81 |
82 | )
83 | }
84 |
85 | export default NewChatForm
86 |
87 | const styles = {
88 | closeIcon: {
89 | position: 'relative',
90 | top: '26px',
91 | left: '18px',
92 | fontSize: '16px',
93 | },
94 | newChatContainer: {
95 | padding: '16px 14px',
96 | backgroundColor: 'white'
97 | }
98 | }
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatFeed/Messages/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 |
3 | import { ChatEngineContext } from '../../../Context'
4 |
5 | import { RenderTrigger } from '../Triggers'
6 |
7 | import MessageBubble from './Bubble/index'
8 |
9 | const Messages = props => {
10 | const {
11 | conn,
12 | chats,
13 | messages,
14 | activeChat,
15 | setIsBottomVisible,
16 | } = useContext(ChatEngineContext)
17 |
18 | const chat = chats && chats[activeChat]
19 | const keys = Object.keys(messages)
20 |
21 | if (!conn || conn === null || !chat) return
22 |
23 | return keys.map((key, index) => {
24 | const message = messages[key]
25 | const lastMessageKey = index === 0 ? null : keys[index - 1]
26 | const nextMessageKey = index === keys.length - 1 ? null : keys[index + 1]
27 |
28 | if (props.renderMessageBubble) {
29 | return (
30 |
31 | {/* Scroll down if the top of last msg is visible */}
32 | {
33 | index === keys.length - 1 &&
34 | setIsBottomVisible(true)}
37 | onLeave={() => setIsBottomVisible(false)}
38 | />
39 | }
40 |
41 | {
42 | props.renderMessageBubble(
43 | conn,
44 | chat,
45 | messages[lastMessageKey],
46 | message,
47 | messages[nextMessageKey]
48 | )
49 | }
50 |
51 | )
52 | }
53 |
54 | return (
55 |
56 | {/* Scroll down if the top of last msg is visible */}
57 | {
58 | index === keys.length - 1 &&
59 | setIsBottomVisible(true)}
62 | onLeave={() => setIsBottomVisible(false)}
63 | />
64 | }
65 |
66 |
72 |
73 | )
74 | })
75 | }
76 | export default Messages
--------------------------------------------------------------------------------
/example/src/Pages/HomePage/ChatEngine.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, useEffect } from 'react'
2 | import { connect } from 'react-redux'
3 |
4 | import _ from 'lodash'
5 |
6 | import {
7 | ChatEngine,
8 | ChatEngineContext,
9 | ChatList,
10 | ChatCard,
11 | NewChatForm,
12 | ChatFeed,
13 | ChatHeader,
14 | IceBreaker,
15 | MessageBubble,
16 | IsTyping,
17 | NewMessageForm,
18 | ScrollDownBar,
19 | ChatSettings,
20 | ChatSettingsTop,
21 | PeopleSettings,
22 | PhotosSettings,
23 | OptionsSettings
24 | } from 'react-chat-engine'
25 |
26 | const ChatEngineApp = (props) => {
27 | const { chats, messages, setActiveChat } = useContext(ChatEngineContext)
28 | const [hasSetLink, setLink] = useState(false)
29 |
30 | useEffect(() => {
31 | const { id } = props
32 | if (id && chats && chats[id] && !_.isEmpty(messages) && !hasSetLink) {
33 | setActiveChat(id)
34 | setLink(true)
35 | }
36 | }, [chats, messages, props, setActiveChat, hasSetLink, setLink])
37 |
38 | return (
39 | }
47 | renderChatCard={(chat, index) => (
48 |
49 | )}
50 | renderNewChatForm={(creds) => }
51 | // renderChatFeed={(chatAppState) => }
52 | renderChatHeader={(chat) => }
53 | renderIceBreaker={(chat) => }
54 | renderMessageBubble={(creds, chat, lastMessage, message, nextMessage) => (
55 |
61 | )}
62 | renderIsTyping={(typers) => }
63 | renderScrollDownBar={(chat, userName, isBottomVisible) => (
64 |
65 | )}
66 | renderNewMessageForm={(creds, chatID) => }
67 | renderChatSettings={(chatAppState) => }
68 | renderChatSettingsTop={(creds, chat) => }
69 | renderPeopleSettings={(creds, chat) => }
70 | renderPhotosSettings={(chat) => }
71 | renderOptionsSettings={(creds, chat) => }
72 | />
73 | )
74 | }
75 |
76 | function mapStateToProps(state) {
77 | return { accounts: state.accounts }
78 | }
79 |
80 | export default connect(mapStateToProps, {})(ChatEngineApp)
81 |
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatFeed/NewMessageForm/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, useEffect } from 'react'
2 |
3 | import { sendMessage, isTyping, ChatEngineContext } from 'react-chat-engine'
4 |
5 | import FilesRow from './FilesRow'
6 | import ImagesRow from './ImagesRow'
7 |
8 | import NextQuill from './NextQuill'
9 |
10 | const NewMessageForm = () => {
11 | const {
12 | conn,
13 | activeChat,
14 | messages,
15 | setMessages
16 | } = useContext(ChatEngineContext)
17 |
18 | const [iter, setIter] = useState(0) // Forces attachments update
19 | const [value, setValue] = useState('')
20 | const [trigger, setTrigger] = useState(0)
21 | const [attachments, setAttachments] = useState([])
22 |
23 | function onRemove(index) {
24 | const newAttachments = attachments
25 | newAttachments.splice(index, 1)
26 | setAttachments(newAttachments)
27 | setIter(iter + 1)
28 | }
29 |
30 | function handleChange(value) {
31 | setValue(value)
32 | setTrigger((trigger + 1) % 4)
33 | if (trigger === 1) { conn && isTyping(conn, activeChat) }
34 | }
35 |
36 | function handleSubmit() {
37 | if (!conn) return
38 |
39 | let text = value.trim()
40 | if (text.length > 11 && text.slice(-11) === '
') { text = text.substr(0, text.length - 11) }
41 |
42 | const custom_json = { sender_id: Date.now().toString() }
43 | const sender_username = conn.userName ? conn.userName : conn.senderUsername
44 | const created = new Date().toISOString().replace('T', ' ').replace('Z', `${Math.floor(Math.random() * 1000)}+00:00`)
45 | const data = { text, attachments, custom_json, sender_username, chat: activeChat, created }
46 |
47 | if (text.length > 0 || attachments.length > 0) {
48 | sendMessage(conn, activeChat, data, () => {})
49 | }
50 |
51 | setValue('')
52 | setAttachments([])
53 |
54 | let newMessages = {...messages}
55 | newMessages[data.created] = data
56 | setMessages(newMessages)
57 | }
58 |
59 | return (
60 |
65 | onRemove(i)} />
66 |
67 | onRemove(i)} />
68 |
69 |
76 |
77 | );
78 | }
79 |
80 | export default NewMessageForm
81 |
82 | const styles = {
83 | NewMessageFormContainer: {
84 | position: 'absolute',
85 | bottom: '0px',
86 | width: '100%',
87 | backgroundColor: 'white',
88 | },
89 | }
--------------------------------------------------------------------------------
/src/components/ChatEngine/components/Input/TextInput/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 |
5 | export default class TextInput extends Component {
6 | state = {
7 | focused: false,
8 | value: null
9 | }
10 |
11 | componentDidMount() {
12 | const value = this.props.default
13 | if(value) {
14 | const event = {target: {value}}
15 | this.props.handleChange(event)
16 | this.setState({ value })
17 | }
18 | }
19 |
20 | render() {
21 | const customStyle = this.props.style ? this.props.style : {}
22 | const defaultStyle = this.state.focused ? styles.focusInput : styles.input
23 |
24 | return (
25 | // NOTE: You may need to make a div the searchContainer to put icons in...
26 | {
34 | this.setState({ focused: false });
35 | this.props.onBlur && this.props.onBlur();
36 | }}
37 | onFocus={() => {
38 | this.setState({ focused: true });
39 | this.props.onFocus && this.props.onFocus();
40 | }}
41 | type={this.props.type ? this.props.type : "text" }
42 | onChange={(e) => this.props.handleChange && this.props.handleChange(e)}
43 | />
44 | )
45 | }
46 | }
47 |
48 | const styles = {
49 | input: {
50 | // Sizes
51 | height: '36px',
52 | fontSize: '15px',
53 | // Border
54 | outline: 'none',
55 | borderRadius: '24px',
56 | border: '1px solid #d9d9d9',
57 | // Padding and fixed width
58 | padding: '0px 12px',
59 | boxSizing: 'border-box',
60 | },
61 | focusInput: {
62 | // Sizes
63 | height: '36px',
64 | fontSize: '15px',
65 | // Border
66 | outline: 'none',
67 | borderRadius: '24px',
68 | border: '1px solid #1890ff',
69 | // Padding and fixed width
70 | padding: '0px 12px',
71 | boxSizing: 'border-box',
72 | }
73 | }
74 |
75 | TextInput.propTypes = {
76 | default: PropTypes.string,
77 | value: PropTypes.string,
78 | label: PropTypes.string,
79 | type: PropTypes.oneOf(['text', 'password', 'number']),
80 | autoFocus: PropTypes.bool,
81 | id: PropTypes.string,
82 | style: PropTypes.object,
83 | handleChange: PropTypes.func,
84 | onFocus: PropTypes.func,
85 | onBlur: PropTypes.func,
86 | }
87 |
--------------------------------------------------------------------------------
/example/tests/tests/testChatEngine.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "tags": ["home", "ChatEngine"],
3 |
4 | 'Test ChatEngine component': function(browser) {
5 | const page = browser.page.homePage();
6 |
7 | page
8 | .navigate()
9 | // Login
10 | .waitForElementVisible('@userNameInput', 1000, 'Find user name input')
11 | .waitForElementVisible('@userSecretInput', 1000, 'Find user secret input')
12 | .waitForElementVisible('@loginButton', 1000, 'Find login button')
13 | .set('@userNameInput', 'Adam_La_Morre')
14 | .set('@userSecretInput', 'pass1234')
15 | .click('@loginButton')
16 | .waitForElementVisible('@logoutButton', 2500, 'Find logout button (i.e. logged in)')
17 |
18 | // New Chat
19 | .waitForElementVisible('@newChatButton', 1000, 'Find new chat button')
20 | .click('@newChatButton')
21 | .waitForElementVisible('@newChatInput', 1000, 'Find new chat input')
22 | .pause(1000).setValue('@newChatInput', ['Test-Chat', browser.Keys.ENTER]).pause(1000)
23 |
24 | // Edit Chat
25 | .waitForElementVisible('#ce-chat-card-title-Test-Chat', 5000, 'Find new Chat Card')
26 | .waitForElementVisible('#ce-chat-feed-title-Test-Chat', 1000, 'Find new Chat Feed')
27 |
28 | // Add Person
29 | .click('.ce-person-title-container')
30 | .pause(1000)
31 | .waitForElementVisible('@addUserInput', 1000, 'Find add user input')
32 | .set('@addUserInput', '.')
33 | .waitForElementVisible('#ce-username-option-Alex_Johns', 5000, 'Find Jane Smith')
34 | .click('#ce-username-option-Alex_Johns')
35 |
36 | // Delete Person
37 | // TODO: We cannot do this at the moment (need hover)
38 |
39 | // Send Message
40 | .waitForElementVisible('@newMessageInput', 1000, 'Find message form')
41 | .waitForElementVisible('@newMessageButton', 1000, 'Find send message button')
42 | .set('@newMessageInput', 'Test-message')
43 | .click('@newMessageButton')
44 | .waitForElementVisible('.ce-message-bubble', 1000, 'Find message bubble')
45 |
46 | // Delete Chat
47 | .getLocationInView('@optionsSettings')
48 | .waitForElementVisible('@optionsSettings', 1000, 'Wait for options drop down')
49 | .click('@optionsSettings')
50 | .getLocationInView('@deleteChatButton')
51 | .waitForElementVisible('@deleteChatButton', 1000, 'Wait for delete chat button')
52 | .click('@deleteChatButton')
53 | .waitForElementNotPresent('#ce-chat-card-title-Test-Chat', 5000, 'Find no more Chat Card')
54 | .waitForElementNotPresent('#ce-chat-feed-title-Test-Chat', 5000, 'Find no more Chat Feed')
55 |
56 | // Logout
57 | .click('@logoutButton')
58 | .waitForElementVisible('@loginButton', 10000, 'Find login button (i.e. logged out)');
59 | },
60 | }
--------------------------------------------------------------------------------
/example/src/Pages/HomePage/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 |
5 | import { login, logout } from '../../Actions/Accounts'
6 |
7 | import { ROOT_URL, DEVELOPMENT, PROJECT_ID } from '../../consts'
8 |
9 | import { ChatEngineWrapper } from 'react-chat-engine'
10 |
11 | import ChatEngine from './ChatEngine'
12 |
13 |
14 | const HomePage = props => {
15 | const [loading, setLoading] = useState(false)
16 | const [userName, setUserName] = useState('')
17 | const [userSecret, setUserSecret] = useState('')
18 | const { id } = props.match.params
19 |
20 | function submit(){
21 | setLoading(true)
22 | console.log({ rootUrl: ROOT_URL, projectID: PROJECT_ID, userName, userSecret })
23 | props.login(
24 | { rootUrl: ROOT_URL, projectID: PROJECT_ID, userName, userSecret },
25 | () => setLoading(false),
26 | (error) => console.log(error)
27 | )
28 | }
29 |
30 | if (!props.accounts.userName) {
31 | return (
32 |
33 | setUserName(e.target.value)}
38 | />
39 |
40 | setUserSecret(e.target.value)}
45 | />
46 |
47 |
54 |
55 | )
56 | }
57 |
58 | return (
59 |
60 |
61 |
67 |
68 |
69 |
70 |
76 |
77 |
78 | )
79 | }
80 |
81 | function mapStateToProps(state){
82 | return { accounts: state.accounts }
83 | }
84 |
85 | function mapDispatchToProps(dispatch){
86 | return bindActionCreators({ login, logout }, dispatch)
87 | }
88 |
89 | export default connect(mapStateToProps, mapDispatchToProps)(HomePage)
--------------------------------------------------------------------------------
/example/src/Pages/ChatTutorial/ChatFeed/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import MyMessage from './MyMessage'
4 | import TheirMessage from './TheirMessage'
5 | import MessageForm from './MessageForm'
6 |
7 | export default class ChatFeed extends Component {
8 | renderReadReceipts(message, isMyMessage) {
9 | const chat = this.props.chats && this.props.chats[this.props.activeChat]
10 |
11 | return chat.people.map((person, index) => {
12 | return person.last_read === message.id &&
13 |
21 | })
22 | }
23 |
24 | renderMessages(messages) {
25 | const keys = Object.keys(messages)
26 |
27 | return keys.map((key, index) => {
28 | const message = messages[key]
29 | const lastMessageKey = index === 0 ? null : keys[index - 1]
30 | const isMyMessage = this.props.userName === message.sender.username
31 |
32 | return (
33 |
34 |
35 | {
36 | isMyMessage ?
37 | :
38 |
39 | }
40 |
41 |
42 |
49 | { this.renderReadReceipts(message, isMyMessage) }
50 |
51 |
52 | )
53 | })
54 | }
55 |
56 | render() {
57 | const chat = this.props.chats && this.props.chats[this.props.activeChat]
58 |
59 | if (!chat) return
60 |
61 | return (
62 |
63 |
64 |
{ chat.title }
65 |
66 |
67 | { chat.people.map(person => ' ' + person.person.username) }
68 |
69 |
70 |
71 | { this.renderMessages(this.props.messages) }
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | );
80 | }
81 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { ChatEngineWrapper, ChatEngineContext } from './components/Context'
2 |
3 | import ChatEngine from './components'
4 |
5 | import { Socket, ChatSocket } from './components/Sockets'
6 |
7 | import Button from './components/ChatEngine/components/Button'
8 | import { Avatar, Dot } from './components/ChatEngine/components/Avatar'
9 | import {
10 | TextInput,
11 | AutoCompleteInput
12 | } from './components/ChatEngine/components/Input'
13 |
14 | import {
15 | getChats,
16 | newChat,
17 | getLatestChats,
18 | getChatsBefore,
19 | getOrCreateChat,
20 | getChat,
21 | editChat,
22 | deleteChat
23 | } from './actions/chats'
24 | import {
25 | addPerson,
26 | removePerson,
27 | getOtherPeople,
28 | getMyData,
29 | editMyData,
30 | leaveChat
31 | } from './actions/people'
32 | import {
33 | getMessages,
34 | getLatestMessages,
35 | sendMessage,
36 | getMessage,
37 | editMessage,
38 | readMessage,
39 | deleteMessage
40 | } from './actions/messages'
41 | import { isTyping } from './actions/typing'
42 |
43 | import ChatList from './components/ChatEngine/ChatList'
44 | import ChatCard from './components/ChatEngine/ChatList/ChatCard'
45 | import NewChatForm from './components/ChatEngine/ChatList/NewChatForm'
46 |
47 | import ChatFeed from './components/ChatEngine/ChatFeed'
48 | import ChatHeader from './components/ChatEngine/ChatFeed/ChatHeader'
49 | import IceBreaker from './components/ChatEngine/ChatFeed/Steps/IceBreaker'
50 | import MessageBubble from './components/ChatEngine/ChatFeed/Messages/Bubble'
51 | import IsTyping from './components/ChatEngine/ChatFeed/IsTyping'
52 | import ScrollDownBar from './components/ChatEngine/ChatFeed/ScrollDownBar'
53 | import NewMessageForm from './components/ChatEngine/ChatFeed/NewMessageForm'
54 | import MessageFormSocial from './components/ChatEngine/ChatFeed/MessageFormSocial'
55 |
56 | import ChatSettings from './components/ChatEngine/ChatSettings'
57 | import ChatSettingsTop from './components/ChatEngine/ChatSettings/ChatSettingsTop'
58 | import PeopleSettings from './components/ChatEngine/ChatSettings/PeopleSettings'
59 | import PhotosSettings from './components/ChatEngine/ChatSettings/PhotosSettings'
60 | import OptionsSettings from './components/ChatEngine/ChatSettings/OptionsSettings'
61 |
62 | export {
63 | ChatEngineWrapper,
64 | ChatEngineContext,
65 | ChatEngine,
66 | Socket,
67 | ChatSocket,
68 | Avatar,
69 | Dot,
70 | Button,
71 | TextInput,
72 | AutoCompleteInput,
73 | getChats,
74 | newChat,
75 | getLatestChats,
76 | getChatsBefore,
77 | getOrCreateChat,
78 | getChat,
79 | editChat,
80 | deleteChat,
81 | addPerson,
82 | removePerson,
83 | getOtherPeople,
84 | getMyData,
85 | editMyData,
86 | leaveChat,
87 | getMessages,
88 | getLatestMessages,
89 | sendMessage,
90 | getMessage,
91 | editMessage,
92 | readMessage,
93 | deleteMessage,
94 | isTyping,
95 | ChatList,
96 | ChatCard,
97 | NewChatForm,
98 | ChatFeed,
99 | ChatHeader,
100 | IceBreaker,
101 | MessageBubble,
102 | IsTyping,
103 | ScrollDownBar,
104 | NewMessageForm,
105 | MessageFormSocial,
106 | ChatSettings,
107 | ChatSettingsTop,
108 | PeopleSettings,
109 | PhotosSettings,
110 | OptionsSettings
111 | }
112 |
--------------------------------------------------------------------------------
/src/components/ChatEngine/components/Avatar/Avatar/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 Avatar 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, is_online } = this.props
25 | const customStyle = this.props.style ? this.props.style : {}
26 | const text = username ? username.substring(0, 2).toUpperCase() : ''
27 | const color = stringToColor(username)
28 |
29 | return (
30 |
31 |
32 |
46 |
50 | { !this.state.avatar && text }
51 |
52 |
53 |
54 |
55 | {
56 | this.props.show_online !== false &&
57 |
64 | }
65 |
66 | )
67 | }
68 | }
69 |
70 | const styles = {
71 | avatar: {
72 | width: '44px',
73 | height: '44px',
74 | borderRadius: '22px',
75 | color: 'white',
76 | textAlign: 'center',
77 | },
78 | avatarText: {
79 | color: 'white',
80 | paddingTop: '12px',
81 | fontSize: '15px',
82 | fontWeight: '600'
83 | },
84 | status: {
85 | width: '8px',
86 | height: '8px',
87 | borderRadius: '100%',
88 | border: '2px solid white',
89 | }
90 | }
91 |
92 | Avatar.propTypes = {
93 | avatar: PropTypes.string,
94 | username: PropTypes.string,
95 | style: PropTypes.object,
96 | is_online: PropTypes.bool,
97 | show_online: PropTypes.bool,
98 | }
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatFeed/ChatHeader/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 |
3 | import { ChatEngineContext } from 'react-chat-engine'
4 |
5 | import ChatListDrawer from './ChatListDrawer'
6 | import ChatSettingsDrawer from './ChatSettingsDrawer'
7 |
8 | import { getDateTime, formatDateTime } from '../../Utilities/timezone'
9 |
10 | import { LoadingOutlined } from '@ant-design/icons'
11 |
12 | import { Row, Col } from 'react-grid-system'
13 |
14 | import { setConfiguration } from 'react-grid-system';
15 |
16 | setConfiguration({ maxScreenClass: 'xl', gutterWidth: 0 });
17 |
18 | const ChatHeader = () => {
19 | const { conn, chats, activeChat } = useContext(ChatEngineContext)
20 |
21 | const chat = chats ? chats[activeChat] : undefined
22 | const otherPerson = chat && conn && chat.people.find(person => person.person.username !== conn.userName)
23 | const title = chat ? (chat.is_direct_chat && otherPerson ? otherPerson.person.username : chat.title) : undefined
24 |
25 | var text = 'Say hello!'
26 | if (!chat) {
27 | text = 'Loading...'
28 | } else if (chat.last_message.created && chat.last_message.created.length > 0) {
29 | const dateTime = getDateTime(chat.last_message.created, conn ? conn.offset : undefined)
30 | text = `Active ${formatDateTime(dateTime)}`
31 | }
32 |
33 | return (
34 |
38 |
44 |
45 |
46 |
47 |
53 |
58 | {title ? title : }
59 |
60 |
61 | {text}
62 |
63 |
64 |
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | export default ChatHeader
77 |
78 | const styles = {
79 | titleSection: {
80 | position: 'absolute',
81 | top: '0px',
82 | width: '100%',
83 | zIndex: '1',
84 | backgroundColor: 'rgb(256, 256, 256, 0.92)',
85 | },
86 | mobileOptiom: {
87 | width: '100%',
88 | top: '32px',
89 | textAlign: 'center',
90 | color: 'rgb(24, 144, 255)',
91 | overflow: 'hidden'
92 | },
93 | titleContainer: {
94 | width: '100%',
95 | padding: '18px 0px',
96 | textAlign: 'center',
97 | color: 'rgb(24, 144, 255)',
98 | overflowX: 'hidden'
99 | },
100 | titleText: {
101 | fontSize: '24px',
102 | fontWeight: '600',
103 | },
104 | subtitleText: {
105 | fontSize: '12px',
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/example/src/Pages/ChatTutorial/App.css:
--------------------------------------------------------------------------------
1 | /* TEAMS */
2 |
3 | * {
4 | font-family: Avenir, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
5 | Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji,
6 | Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
7 | letter-spacing: 0.5px;
8 | }
9 |
10 | .ce-chat-list {
11 | background-color: rgb(240, 240, 240) !important;
12 | }
13 |
14 | .ce-chats-container {
15 | background-color: rgb(240, 240, 240) !important;
16 | }
17 |
18 | .ce-active-chat-card {
19 | background-color: #cabcdc !important;
20 | border: 4px solid #cabcdc !important;
21 | border-radius: 0px !important;
22 | }
23 |
24 | .ce-chat-subtitle-text {
25 | color: #595959 !important;
26 | }
27 |
28 | .ce-chat-form-container {
29 | padding-bottom: 20px !important;
30 | }
31 |
32 | .ce-text-input {
33 | border-radius: 6px !important;
34 | border: 1px solid #3b2a50 !important;
35 | }
36 |
37 | .ce-primary-button {
38 | border-radius: 6px !important;
39 | background-color: #7554a0 !important;
40 | position: relative;
41 | bottom: 1px;
42 | }
43 |
44 | .ce-danger-button {
45 | background-color: white !important;
46 | border-radius: 22px !important;
47 | }
48 |
49 | .ce-settings {
50 | background-color: rgb(240, 240, 240) !important;
51 | }
52 |
53 | .ce-autocomplete-input {
54 | border-radius: 6px !important;
55 | border: 1px solid #3b2a50 !important;
56 | }
57 |
58 | .ce-autocomplete-options {
59 | border-radius: 6px !important;
60 | border: 1px solid #3b2a50 !important;
61 | background-color: white !important;
62 | }
63 |
64 | .ce-chat-settings-container {
65 | padding-top: 12px !important;
66 | }
67 |
68 | .ce-chat-avatars-row {
69 | display: none !important;
70 | }
71 |
72 | /* CUSTOM FEED */
73 |
74 | .chat-feed {
75 | height: 100%;
76 | width: 100%;
77 | overflow: scroll;
78 | background-color: rgb(240, 240, 240);
79 | }
80 |
81 | .chat-title-container {
82 | width: calc(100% - 36px);
83 | padding: 18px;
84 | text-align: center;
85 | }
86 |
87 | .chat-title {
88 | color: #7554a0;
89 | font-weight: 800;
90 | font-size: 24px;
91 | }
92 |
93 | .chat-subtitle {
94 | color: #7554a0;
95 | font-weight: 600;
96 | font-size: 12px;
97 | padding-top: 4px;
98 | }
99 |
100 | .message-row {
101 | float: left;
102 | width: 100%;
103 | display: flex;
104 | margin-left: 18px;
105 | }
106 |
107 | .message-block {
108 | width: 100%;
109 | display: inline-block;
110 | }
111 |
112 | .message-avatar {
113 | width: 44px;
114 | height: 44px;
115 | border-radius: 22px;
116 | color: white;
117 | text-align: center;
118 | background-repeat: no-repeat;
119 | background-position: center;
120 | background-size: 48px;
121 | }
122 |
123 | .message {
124 | padding: 12px;
125 | font-size: 16px;
126 | border-radius: 6px;
127 | max-width: 60%;
128 | }
129 |
130 | .message-image {
131 | margin-right: 18px;
132 | object-fit: cover;
133 | border-radius: 6px;
134 | height: 30vw;
135 | width: 30vw;
136 | max-height: 200px;
137 | max-width: 200px;
138 | min-height: 100px;
139 | min-width: 100px;
140 | }
141 |
142 | .read-receipts {
143 | position: relative;
144 | bottom: 6px;
145 | }
146 |
147 | .read-receipt {
148 | width: 13px;
149 | height: 13px;
150 | border-radius: 13px;
151 | margin: 1.5px;
152 | background-repeat: no-repeat;
153 | background-position: center;
154 | background-size: 14px;
155 | }
156 |
157 | .message-form-container {
158 | position: absolute;
159 | bottom: 0px;
160 | width: calc(100% - 36px);
161 | padding: 18px;
162 | background-color: rgb(240, 240, 240);
163 | }
164 |
165 | .message-form {
166 | overflow: hidden;
167 | border-radius: 6px;
168 | border: 1px solid #3b2a50;
169 | background-color: white;
170 | }
171 |
172 | .message-input {
173 | height: 40px;
174 | width: calc(100% - 132px);
175 | background-color: white;
176 | border: 1px solid white;
177 | padding: 0px 18px;
178 | outline: none;
179 | font-size: 15px;
180 | }
181 |
182 | .image-button {
183 | cursor: pointer;
184 | padding: 0px 12px;
185 | height: 100%;
186 | }
187 |
188 | .send-button {
189 | height: 42px;
190 | background-color: white;
191 | border: 1px solid white;
192 | padding: 0px 18px;
193 | cursor: pointer;
194 | }
195 |
196 | .send-icon {
197 | top: 1px;
198 | position: relative;
199 | transform: rotate(-90deg);
200 | }
201 |
202 | .picture-icon {
203 | top: 1px;
204 | position: relative;
205 | font-size: 14px;
206 | }
207 |
--------------------------------------------------------------------------------
/src/components/ChatEngine/components/Input/AutocompleteInput/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { CloseOutlined } from '@ant-design/icons'
5 |
6 | export default class AutoCompleteInput extends Component {
7 | state = {
8 | focused: false,
9 | showAll: false,
10 | }
11 |
12 | onFocus() {
13 | this.onChange('', true)
14 | this.setState({ focused: true })
15 | this.props.onFocus && this.props.onFocus()
16 | }
17 |
18 | onBlur() {
19 | this.setState({ focused: false })
20 | this.props.onBlur && this.props.onBlur()
21 | }
22 |
23 | onChange(value, showAll) {
24 | this.setState({ showAll })
25 | this.props.handleChange && this.props.handleChange(value)
26 | }
27 |
28 | getNames() {
29 | let count = 0
30 | const max = this.props.max ? this.props.max : 3
31 |
32 | const results = []
33 |
34 | this.props.options.map(option => {
35 | if (JSON.stringify(option).toLowerCase().indexOf(this.props.value.toLowerCase()) !== -1 && count < max) {
36 | count = count + 1
37 | results.push(option)
38 | }
39 | })
40 |
41 | return results
42 | }
43 |
44 |
45 | renderOptions() {
46 | if(!this.props.value && !this.state.showAll) { return }
47 |
48 | const results = this.getNames()
49 |
50 | return results.map((option, index) => {
51 | return (
52 |
53 | { this.props.renderOption && this.props.renderOption(option) }
54 |
55 | {
56 | index === results.length - 1 &&
57 |
this.onChange('', false)}
61 | >
62 |
63 |
64 | }
65 |
66 | )
67 | })
68 | }
69 |
70 | render() {
71 | const { options } = this.props
72 | const customStyle = this.props.style ? this.props.style : {}
73 | const defaultStyle = { ...styles.input, ...{ border: this.state.focused ? '1px solid #1890ff' : '1px solid #d9d9d9' } }
74 |
75 | return (
76 |
77 |
this.onBlur()}
85 | onFocus={() => this.onFocus()}
86 | onChange={(e) => this.onChange(e.target.value, true)}
87 | />
88 |
89 | {
90 | options && options.length > 0 && this.state.showAll &&
91 |
95 | { this.props.options && this.renderOptions() }
96 |
97 | }
98 |
99 | )
100 | }
101 | }
102 |
103 | const styles = {
104 | input: {
105 | height: '36px',
106 | fontSize: '15px',
107 | outline: 'none',
108 | borderRadius: '24px',
109 | padding: '0px 12px',
110 | boxSizing: 'border-box',
111 | fontFamily: 'Avenir'
112 | },
113 | close: {
114 | cursor: 'pointer',
115 | textAlign: 'center',
116 | padding: '8px 12px',
117 | fontSize: '15px',
118 | borderRadius: '24px',
119 | }
120 | }
121 |
122 | AutoCompleteInput.propTypes = {
123 | default: PropTypes.string,
124 | value: PropTypes.string,
125 | options: PropTypes.arrayOf(PropTypes.object),
126 | max: PropTypes.number,
127 | label: PropTypes.string,
128 | type: PropTypes.oneOf(['text', 'number']),
129 | style: PropTypes.object,
130 | handleChange: PropTypes.func,
131 | onFocus: PropTypes.func,
132 | onBlur: PropTypes.func,
133 | }
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatFeed/MessageFormSocial/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react'
2 |
3 | import { ChatEngineContext } from 'react-chat-engine'
4 |
5 | import { sendMessage, isTyping } from 'react-chat-engine'
6 |
7 | import FilesRow from '../NewMessageForm/FilesRow'
8 | import ImagesRow from '../NewMessageForm/ImagesRow'
9 |
10 | import ImagesInput from './ImagesInput'
11 | import MessageInput from './MessageInput'
12 |
13 | const MessageFormSocial = () => {
14 | const { conn, activeChat, messages, setMessages } = useContext(ChatEngineContext)
15 |
16 | const [iter, setIter] = useState(0) // Forces attachments update
17 | const [value, setValue] = useState('')
18 | const [trigger, setTrigger] = useState(0)
19 | const [attachments, setAttachments] = useState([])
20 |
21 | if (!conn || conn === null) return
22 |
23 | function onRemove(index) {
24 | const newAttachments = attachments
25 | newAttachments.splice(index, 1)
26 | setAttachments(newAttachments)
27 | setIter(iter + 1)
28 | }
29 |
30 | function handleChange(e) {
31 | setValue(e.target.value)
32 | setTrigger((trigger + 1) % 4)
33 | if (trigger === 1) { conn && isTyping(conn, activeChat) }
34 | }
35 |
36 | function handleSubmit(e) {
37 | e.preventDefault();
38 |
39 | if (!conn) return
40 |
41 | let text = value.trim()
42 | if (text.length > 11 && text.slice(-11) === '
') { text = text.substr(0, text.length - 11) }
43 |
44 | const custom_json = { sender_id: Date.now().toString() }
45 | const sender_username = conn.userName ? conn.userName : conn.senderUsername
46 | const created = new Date().toISOString().replace('T', ' ').replace('Z', `${Math.floor(Math.random() * 1000)}+00:00`)
47 | const data = { text, attachments, custom_json, sender_username, chat: activeChat, created }
48 |
49 | if (text.length > 0 || attachments.length > 0) {
50 | sendMessage(conn, activeChat, data, () => {})
51 | }
52 |
53 | setValue('')
54 | setAttachments([])
55 |
56 | let newMessages = {...messages}
57 | newMessages[data.created] = data
58 | setMessages(newMessages)
59 | }
60 |
61 | return (
62 |
102 | );
103 | }
104 |
105 | export default MessageFormSocial
106 |
107 | const styles = {
108 | messageFormContainer: {
109 | position: 'absolute',
110 | bottom: '0px',
111 | width: '100%',
112 | backgroundColor: 'white',
113 | },
114 | messageForm: {
115 | paddingTop: "22px",
116 | marginRight: "10px",
117 | width: '100%',
118 | },
119 | inputContainer: {
120 | minHeight: '36px',
121 | paddingBottom: '16px',
122 | width: '100%'
123 | },
124 | sendMessage: {
125 | // Look + Feel
126 | width: "40px",
127 | height: "40px",
128 | borderRadius: "50%",
129 | borderWidth: "0px",
130 | padding: "0px",
131 | outline: "none",
132 | color: "white",
133 | backgroundColor: 'rgb(24, 144, 255)',
134 | cursor: "pointer",
135 | // Positioning
136 | position: 'absolute',
137 | bottom: '12px',
138 | right: '10px',
139 | zIndex: '10'
140 | },
141 | sendIcon: {
142 | width: "20px",
143 | transform: "rotate(-20deg)",
144 | marginLeft: "5px",
145 | }
146 | }
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatSettings/ChatSettingsTop/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 |
3 | import { Avatar, ChatEngineContext } from 'react-chat-engine'
4 |
5 | import TitleForm from './TitleForm'
6 |
7 | const ChatSettingsTop = () => {
8 | const { conn, chats, activeChat } = useContext(ChatEngineContext)
9 | const chat = chats && chats[activeChat]
10 |
11 | if (!chat || !conn || conn === null) return
12 |
13 | const topPeople = chat.people.slice(0, 3)
14 | const otherPerson = getOtherPerson(chat.people)
15 |
16 | function renderOnePerson(people) {
17 | return (
18 |
27 | )
28 | }
29 |
30 | function renderTwoPeople(people) {
31 | return (
32 |
49 | )
50 | }
51 |
52 | function renderThreePeople(people) {
53 | return (
54 |
55 |
62 |
63 |
70 |
71 |
78 |
79 | )
80 | }
81 |
82 | function getOtherPerson(people) {
83 | return people.find(person => person.person.username !== conn.userName);
84 | }
85 |
86 | return (
87 |
88 |
89 | { topPeople.length === 1 && renderOnePerson(topPeople) }
90 |
91 | { chat.is_direct_chat && otherPerson && renderOnePerson([otherPerson]) }
92 |
93 | { !chat.is_direct_chat && otherPerson && topPeople.length === 2 && renderTwoPeople(topPeople) }
94 |
95 | { !chat.is_direct_chat && otherPerson && topPeople.length === 3 && renderThreePeople(topPeople) }
96 |
97 |
98 | {
99 | chat.is_direct_chat && otherPerson ?
100 |
109 | { otherPerson.person.username }
110 |
:
111 |
112 | }
113 |
114 | )
115 | }
116 |
117 | export default ChatSettingsTop
118 |
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatList/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useRef, useState } from 'react'
2 |
3 | import { ChatEngineContext, getLatestChats, getChatsBefore } from 'react-chat-engine'
4 |
5 | import { getDateTime } from '../Utilities/timezone'
6 |
7 | import _ from 'lodash'
8 |
9 | import ChatLoader from './ChatLoader'
10 | import NewChatForm from './NewChatForm'
11 | import ChatCard from './ChatCard'
12 |
13 | const interval = 33
14 |
15 | const ChatList = props => {
16 | const didMountRef = useRef(false)
17 | const [loadChats, setLoadChats] = useState(false) // true, false, or loading
18 | const [hasMoreChats, setHasMoreChats] = useState(true)
19 | const { conn, chats, setChats, setActiveChat } = useContext(ChatEngineContext)
20 |
21 | const chatList = sortChats(
22 | chats ?
23 | Object.values(chats) :
24 | [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]
25 | )
26 |
27 | useEffect(() => {
28 | if (!didMountRef.current) {
29 | didMountRef.current = true
30 |
31 | getLatestChats(
32 | props,
33 | interval,
34 | (chats) => {
35 | onGetChats(chats)
36 | chats.length > 0 && setActiveChat(chats[0].id)
37 | }
38 | )
39 | }
40 | })
41 |
42 | useEffect(() => {
43 | if (!loadChats || loadChats === 'loading') return;
44 | setLoadChats('loading')
45 |
46 | const chatList = chats !== null ? sortChats(Object.values(chats)) : []
47 | if (chatList.length > 0) {
48 | const before = chatList[chatList.length - 1].created
49 | getChatsBefore(props, before, interval, (chats) => onGetChats(chats))
50 | }
51 | }, [loadChats])
52 |
53 | function sortChats(chats) {
54 | return chats.sort((a, b) => {
55 | const aDate = a.last_message && a.last_message.created ? getDateTime(a.last_message.created, props.offset) : getDateTime(a.created, props.offset)
56 | const bDate = b.last_message && b.last_message.created ? getDateTime(b.last_message.created, props.offset) : getDateTime(b.created, props.offset)
57 | return new Date(bDate) - new Date(aDate);
58 | })
59 | }
60 |
61 | function onGetChats(chatList) {
62 | setLoadChats(false)
63 | const oldChats = chats !== null ? chats : {}
64 | const newChats = _.mapKeys({...chatList}, 'id')
65 | const allChats = {...oldChats, ...newChats}
66 | setChats(allChats);
67 | interval > chatList.length && setHasMoreChats(false);
68 | }
69 |
70 | function renderChats(chats) {
71 | return chats.map((chat, index) => {
72 | if (!chat) {
73 | return
74 |
75 | } else if (props.renderChatCard) {
76 | return {props.renderChatCard(chat, index)}
77 |
78 | } else {
79 | return (
80 | props.onChatClick && props.onChatClick(chat.id)}
83 | >
84 |
85 |
86 | )
87 | }
88 | })
89 | }
90 |
91 | return (
92 |
93 |
94 | {
95 | props.renderNewChatForm ?
96 | props.renderNewChatForm(conn) :
97 |
props.onClose() : undefined} />
98 | }
99 |
100 | { renderChats(chatList) }
101 |
102 | {
103 | hasMoreChats && chatList.length > 0 &&
104 |
105 |
!loadChats && setLoadChats(true)} />
107 |
108 |
109 | }
110 |
111 |
112 | )
113 | }
114 |
115 | const styles={
116 | chatListContainer: {
117 | height: '100%',
118 | maxHeight: '100vh',
119 | overflow: 'scroll',
120 | overflowX: 'hidden',
121 | borderRight: '1px solid #afafaf',
122 | backgroundColor: 'white',
123 | fontFamily: 'Avenir'
124 | },
125 | chatsContainer: {
126 | width: '100%',
127 | height: '100%',
128 | backgroundColor: 'white',
129 | borderRadius: '0px 0px 24px 24px'
130 | },
131 | }
132 |
133 | export default ChatList;
134 |
--------------------------------------------------------------------------------
/src/components/ChatEngine/ChatList/ChatCard/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 |
3 | import { ChatEngineContext } from 'react-chat-engine'
4 |
5 | import _ from 'lodash'
6 |
7 | import Loading from './Loading'
8 |
9 | import { Boop } from '../../components/Effects'
10 |
11 | import { getDateTime } from '../../Utilities/timezone'
12 |
13 | const { htmlToText } = require('html-to-text')
14 |
15 | const ChatCard = props => {
16 | const { chat } = props
17 | const { conn, activeChat, setActiveChat } = useContext(ChatEngineContext)
18 |
19 | if (_.isEmpty(chat) || props.loading) return
20 | if (!conn || conn === null) return
21 |
22 | const extraStyle = activeChat === chat.id ? styles.activeChat : {}
23 | const otherPerson = chat.people.find(person => conn.userName !== person.person.username);
24 | const title = chat.is_direct_chat && otherPerson ? otherPerson.person.username : chat.title
25 |
26 | let lastMessage = htmlToText(chat.last_message.text, {})
27 | if (!lastMessage) {
28 | lastMessage = chat.last_message.attachments.length > 0 ?
29 | `${chat.last_message.attachments.length} image${chat.last_message.attachments.length > 1 ? 's' : ''}` :
30 | 'Say hello!'
31 | }
32 |
33 | function didReadLastMessage(chat) {
34 | let didReadLastMessage = true
35 | chat.people.map(chat_person => {
36 | if(conn.userName === chat_person.person.username) {
37 | didReadLastMessage = chat.last_message.id === chat_person.last_read
38 | }
39 | })
40 | return didReadLastMessage
41 | }
42 |
43 | function daySinceSent(date) {
44 | if (!date) return ''
45 | return getDateTime(date, conn.offset).toString().substr(4, 6)
46 | }
47 |
48 | return (
49 |
50 | setActiveChat(chat.id)}
52 | style={{ ...styles.chatContainer, ...extraStyle }}
53 | className={`ce-chat-card ${activeChat === chat.id && 'ce-active-chat-card'}`}
54 | >
55 |
60 |
67 | { title }
68 |
69 |
70 | {
71 | !didReadLastMessage(chat) &&
72 |
84 | }
85 |
86 |
87 |
88 |
89 | { lastMessage }
90 |
91 |
92 |
96 | { daySinceSent(chat.last_message.created) }
97 |
98 |
99 |
100 |
101 | )
102 | }
103 |
104 | const styles={
105 | chatContainer: {
106 | padding: '16px',
107 | paddingBottom: '12px',
108 | cursor: 'pointer'
109 | },
110 | titleText: {
111 | fontWeight: '500',
112 | paddingBottom: '4px',
113 | whiteSpace: 'nowrap',
114 | overflow: 'hidden'
115 | },
116 | messageText: {
117 | width: '75%',
118 | color: 'rgba(153, 153, 153, 1)',
119 | fontSize: '14px',
120 | whiteSpace: 'nowrap',
121 | overflow: 'hidden',
122 | display: 'inline-block'
123 | },
124 | activeChat: {
125 | backgroundColor: '#d9d9d9',
126 | border: '0px solid white',
127 | borderRadius: '12px'
128 | },
129 | }
130 |
131 | export default ChatCard;
132 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 | ## Chat Engine
14 |
15 | Chat Engine is a free serverless chat API.
16 |
17 | Try our free plan at [chatengine.io](https://chatengine.io)
18 |
19 | ## Installation
20 |
21 | - Using [npm](https://www.npmjs.com/#getting-started): `npm install react-chat-engine --save`
22 | - Using [Yarn](https://yarnpkg.com/): `yarn add react-chat-engine`
23 |
24 | ## Quick Start
25 |
26 | Add serverless chat to your React app in 3 minutes.
27 |
28 | 1. Register then create a **project** and **user** at [chatengine.io](https://chatengine.io)
29 |
30 | 2. Collect the **public key**, **username** and **user password**
31 |
32 | 3. Install `yarn add react-chat-engine`
33 |
34 | 4. Import the `ChatEngine` component and pass in `publicKey`, `userName`, and `userSecret` props
35 |
36 | 5. Voila! You're done
37 |
38 | EXAMPLE: Your implementation should look like the following
39 |
40 | ```jsx
41 | import React from 'react'
42 |
43 | import { ChatEngine } from 'react-chat-engine'
44 |
45 | export function App() {
46 | return (
47 |
52 | )
53 | }
54 | ```
55 |
56 | ## Features
57 |
58 | - Authenticate users
59 | - Subscribe (connect) to incoming chats and messages
60 | - Create chats and messages
61 | - Add and remove people from chats
62 | - Edit and delete chat and message data
63 |
64 | ## Props
65 |
66 | - **`publicKey`** _(UUID REQUIRED)_ - Public API key for your [chatengine.io](https://chatengine.io) project
67 | - **`userName`** _(String REQUIRED)_ - Username of a person in this project
68 | - **`userSecret`** _(String REQUIRED)_ - Set a secret for this person and use it to authenticate.
69 | - **`onConnect`** (Function) - Callback when the connection/authentication is complete
70 | - **`onFailAuth`** (Function) - Callback when the connection/authentication fails
71 | - **`onGetChats`** _(Function)_ Callback when the person fetches their chats array
72 | - **`onNewChat`** _(Function)_ - Callback when the person creates a new chat
73 | - **`onEditChat`** _(Function)_ - Callback when the person edits a chat title
74 | - **`onDeleteChat`** _(Function)_ - Callback when the person deletes one of their chats (must the chat's admin)
75 | - **`onAddPerson`** _(Function)_ - Callback when a person is added to a chat
76 | - **`onRemovePerson`** _(Function)_ - Callback when a person is removed/deleted from a chat
77 | - **`onGetMessages`** _(Function)_ - Callback when the person gets a chat's messages
78 | - **`onNewMessage`** _(Function)_ - Callback when a person posts a new message in one of the chats
79 | - **`onEditMessage`** _(Function)_ - Callback when a person edits a new message in one of the chats
80 | - **`onDeleteMessage`** _(Function)_ - Callback when a person deletes a new message in one of the chats
81 | - **`hideUI`** _(Boolean)_ - Hides all UI components for a custom implementation (Warning: Advanced)
82 |
83 | ## Functions
84 |
85 | ```
86 | import { functionName } from 'react-chat-engine'
87 |
88 | ...
89 |
90 | functionName(conn, args)
91 | ```
92 |
93 | - **`getChats`** _(conn) => void_ - Get a person's array of chats
94 | - **`newChat`** _(conn, title) => void_ - Create a new chat with this person as admin
95 | - **`editChat`** _(conn, chatId, chatObj) => void_ - Edit the title of an existing chat
96 | - **`deleteChat`** _(conn, chatId) => void_ - If you're admin, delete this existing chat
97 | - **`addPerson`** _(props, chatId, userName) => void_ - Add an existing person (in the project) to an existing chat
98 | - **`removePerson`** _(props, chatId, userName) => void_ - If you're admin, remove this user from an existing chat
99 | - **`getMessages`** _(props, chatId) => void_ - Get the messages for an existing chat
100 | - **`sendMessage`** _(props, chatId, messageObj) => void_ - Send a new message object into this chat
101 | - **`editMessage`** _(props, chatId, messageId, messageObj) => void_ - Edit an exiting message object in this chat
102 | - **`deleteMessage`** _(props, chatId, messageId) => void_ - Delete an exiting message object from this chat
103 |
104 | ## Objects
105 |
106 | ### Chat Object
107 |
108 | - **`id`** _(int)_ - Unique primary key to identify this chat
109 | - **`admin`** _(String)_ - Unique username of the person who created this chat
110 | - **`title`** _(String)_ - Optional title of this chat
111 | - **`created`** _(Datetime)_ - Date-time of chat creation
112 | - **`people`** _(Array)_ - Array of people added to this chat
113 |
114 | ```
115 | {
116 | "id": 1,
117 | "admin": "john_smith",
118 | "title": "Canada Day Party!",
119 | "created": "2020-09-05T20:28:22.352373Z",
120 | "people": [
121 | {
122 | "person": "john_smith"
123 | }
124 | ]
125 | }
126 | ```
127 |
128 | ### Chat / Person Association
129 |
130 | - **`person`** _(String)_ - Unique username of a person involved in this chat
131 |
132 | ```
133 | { person: "john_smith" }
134 | ```
135 |
136 | ### Message Object
137 |
138 | - **`id`** _(int)_ - Unique primary key to identify this message
139 | - **`sender`** _(String)_ - Unique username of the person who sent this message
140 | - **`text`** (String) - Contents of the message sent
141 | - **`created`** (Datetime) - Date-time of message creation
142 |
143 | ```
144 | {
145 | "id": 1,
146 | "sender": "john_smith",
147 | "text": "Hey let's party!",
148 | "created": "2020-09-07T13:20:26.936400Z"
149 | }
150 | ```
151 |
--------------------------------------------------------------------------------