+
@@ -32,11 +42,14 @@ class PageTitle extends React.Component {
PageTitle.propTypes = {
title: PropTypes.string.isRequired,
shadow: PropTypes.bool,
+ hideBorderBottom: PropTypes.bool,
+ extraClasses: PropTypes.string,
rightComponent: PropTypes.element
}
PageTitle.defaultProps = {
- shadow: false
+ shadow: false,
+ hideBorderBottom: false
}
export default PageTitle
diff --git a/src/client/containers/Messages/index.jsx b/src/client/containers/Messages/index.jsx
new file mode 100644
index 000000000..029cfedb6
--- /dev/null
+++ b/src/client/containers/Messages/index.jsx
@@ -0,0 +1,453 @@
+import React, { createRef } from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { observer } from 'mobx-react'
+import { makeObservable, observable } from 'mobx'
+import clsx from 'clsx'
+import Log from '../../logger'
+
+import {
+ fetchConversations,
+ fetchSingleConversation,
+ unloadSingleConversation,
+ unloadConversations,
+ sendMessage,
+ receiveMessage
+} from 'actions/messages'
+import {
+ MESSAGES_USER_TYPING,
+ MESSAGES_UI_USER_TYPING,
+ MESSAGES_SEND,
+ MESSAGES_UI_RECEIVE
+} from 'serverSocket/socketEventConsts'
+
+import SpinLoader from 'components/SpinLoader'
+import PageTitle from 'components/PageTitle'
+import Grid from 'components/Grid'
+import GridItem from 'components/Grid/GridItem'
+
+import UIKit from 'uikit'
+import $ from 'jquery'
+import helpers from 'lib/helpers'
+
+@observer
+class MessagesContainer extends React.Component {
+ @observable userListShown = false
+ @observable singleConversationLoaded = false
+ @observable typingTimers = {}
+
+ conversationScrollSpy = createRef()
+ userTypingBubbles = createRef()
+ messagesContainer = createRef()
+
+ constructor (props) {
+ super(props)
+
+ makeObservable(this)
+
+ this.onReceiveMessage = this.onReceiveMessage.bind(this)
+ this.onUserIsTyping = this.onUserIsTyping.bind(this)
+ this.onUserStopTyping = this.onUserStopTyping.bind(this)
+ this._stopTyping = this._stopTyping.bind(this)
+ }
+
+ componentDidMount () {
+ this.props.fetchConversations()
+
+ this.props.socket.on(MESSAGES_UI_USER_TYPING, this.onUserIsTyping)
+ this.props.socket.on(MESSAGES_UI_RECEIVE, this.onReceiveMessage)
+
+ helpers.resizeFullHeight()
+
+ if (this.props.initialConversation) {
+ this.props.fetchSingleConversation({ _id: this.props.initialConversation }).then(() => {
+ this.scrollToMessagesBottom(true)
+ })
+ }
+ }
+
+ componentDidUpdate (prevProps, prevState, snapshot) {
+ helpers.resizeAll()
+ helpers.setupScrollers()
+ this.setupContextMenu()
+ }
+
+ componentWillUnmount () {
+ this.props.unloadConversations()
+ this.props.unloadSingleConversation()
+
+ this.props.socket.off(MESSAGES_UI_USER_TYPING, this.onUserIsTyping)
+ this.props.socket.off(MESSAGES_UI_RECEIVE, this.onReceiveMessage)
+ }
+
+ onReceiveMessage (data) {
+ data.isOwner = data.message.owner._id.toString() === this.props.sessionUser._id.toString()
+ this.props.receiveMessage(data)
+
+ // Hide Bubbles
+ const currentConversation = this.props.messagesState.currentConversation
+ if (
+ !data.isOwner &&
+ currentConversation &&
+ currentConversation.get('_id').toString() === data.message.conversation.toString()
+ ) {
+ if (this.userTypingBubbles.current && !this.userTypingBubbles.current.classList.contains('hide'))
+ this.userTypingBubbles.current.classList.add('hide')
+ }
+ }
+
+ onUserIsTyping (data) {
+ const typingTimerKey = `${data.cid}_${data.from}`
+ if (this.typingTimers[typingTimerKey]) {
+ clearTimeout(this.typingTimers[typingTimerKey])
+ }
+
+ this.typingTimers[typingTimerKey] = setTimeout(this._stopTyping, 10000, data.cid, data.from)
+
+ // Show Bubbles
+ if (this.props.messagesState.currentConversation) {
+ if (this.props.messagesState.currentConversation.get('_id').toString() === data.cid.toString()) {
+ this.scrollToMessagesBottom(false)
+ if (this.userTypingBubbles.current && this.userTypingBubbles.current.classList.contains('hide'))
+ this.userTypingBubbles.current.classList.remove('hide')
+ }
+ }
+ }
+
+ _stopTyping (cid, from) {
+ const typingTimerKey = `${cid}_${from}`
+ this.typingTimers[typingTimerKey] = undefined
+
+ // Hide Bubbles
+ if (this.props.messagesState.currentConversation) {
+ if (this.props.messagesState.currentConversation.get('_id').toString() === cid.toString()) {
+ if (this.userTypingBubbles.current && !this.userTypingBubbles.current.classList.contains('hide'))
+ this.userTypingBubbles.current.classList.add('hide')
+ }
+ }
+ }
+
+ onUserStopTyping (data) {
+ console.log(data)
+ }
+
+ showUserList (e) {
+ e.preventDefault()
+ this.userListShown = true
+ }
+
+ hideUserList (e) {
+ e.preventDefault()
+ this.userListShown = false
+ }
+
+ setupContextMenu () {
+ // Setup Context Menu
+ helpers.setupContextMenu('#conversationList > ul > li', function (action, target) {
+ let $li = $(target)
+ if (!$li.is('li')) {
+ $li = $(target).parents('li')
+ }
+ const convoId = $li.attr('data-conversation-id')
+ if (action.toLowerCase() === 'delete') {
+ UIKit.modal.confirm(
+ 'Are you sure you want to delete this conversation?',
+ function () {
+ // Confirm
+ console.log(convoId)
+ // deleteConversation(convoId)
+ },
+ function () {
+ // Cancel
+ },
+ {
+ labels: {
+ Ok: 'YES'
+ },
+ confirmButtonClass: 'md-btn-danger'
+ }
+ )
+ }
+ })
+ }
+
+ scrollToMessagesBottom (hideLoader) {
+ setTimeout(() => {
+ if (this.messagesContainer.current) helpers.scrollToBottom($(this.messagesContainer.current), false)
+ if (hideLoader) this.singleConversationLoaded = true
+ }, 100)
+ }
+
+ onConversationClicked (id) {
+ if (
+ this.props.messagesState.currentConversation &&
+ this.props.messagesState.currentConversation.get('_id').toString() === id.toString()
+ )
+ return
+
+ // History.replaceState(null, null, `/messages/${id}`)
+
+ this.props.unloadSingleConversation().then(() => {
+ this.singleConversationLoaded = false
+ this.props.fetchSingleConversation({ _id: id }).then(() => {
+ this.scrollToMessagesBottom(true)
+ })
+ })
+ }
+
+ onSendMessageKeyDown (e, cid, to) {
+ if (e.code !== 'Enter' || e.code !== 'NumpadEnter') {
+ this.props.socket.emit(MESSAGES_USER_TYPING, { cid, to, from: this.props.sessionUser._id })
+ }
+ }
+
+ onSendMessageSubmit (e, cId, to) {
+ e.preventDefault()
+ if (!cId || !to) return
+
+ if (e.target.chatMessage && e.target.chatMessage.value !== '') {
+ this.props
+ .sendMessage({
+ cId,
+ owner: this.props.sessionUser._id,
+ body: e.target.chatMessage.value.trim()
+ })
+ .then(res => {
+ this.props.socket.emit(MESSAGES_SEND, {
+ to,
+ from: this.props.sessionUser._id,
+ message: res.message
+ })
+
+ $(e.target.chatMessage).val('')
+
+ this.scrollToMessagesBottom()
+ })
+ }
+ }
+
+ render () {
+ const { currentConversation } = this.props.messagesState
+
+ return (
+
+ }
+ />
+
+
+
+ {currentConversation && (
+
+
+
+
+ Conversation Started on {currentConversation.get('createdAt')}
+
+ {currentConversation.get('requestingUserMeta').get('deletedAt') && (
+
+ Conversation Deleted at {currentConversation.get('requestingUserMeta').get('deletedAt')}
+
+ )}
+
+
+
+
+ {currentConversation.get('messages').map(message => {
+ const ownerImage = message.get('owner').get('image') || 'defaultProfile.jpg'
+ const isMessageOwner =
+ message
+ .get('owner')
+ .get('_id')
+ .toString() === this.props.sessionUser._id
+ const formattedDate = helpers.formatDate(
+ message.get('createdAt'),
+ helpers.getShortDateWithTimeFormat()
+ )
+ return (
+
+ {!isMessageOwner && (
+
+
+
{message.get('body')}
+
+ )}
+ {isMessageOwner && (
+
+
+ {message.get('body')}
+
+
+ )}
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ -
+ Delete Conversation
+
+
+
+ )
+ }
+}
+
+MessagesContainer.propTypes = {
+ sessionUser: PropTypes.object,
+ socket: PropTypes.object.isRequired,
+ fetchConversations: PropTypes.func.isRequired,
+ unloadConversations: PropTypes.func.isRequired,
+ fetchSingleConversation: PropTypes.func.isRequired,
+ unloadSingleConversation: PropTypes.func.isRequired,
+ sendMessage: PropTypes.func.isRequired,
+ receiveMessage: PropTypes.func.isRequired,
+ messagesState: PropTypes.object.isRequired,
+ initialConversation: PropTypes.string
+}
+
+const mapStateToProps = state => ({
+ sessionUser: state.shared.sessionUser,
+ socket: state.shared.socket,
+ messagesState: state.messagesState
+})
+
+export default connect(mapStateToProps, {
+ fetchConversations,
+ unloadConversations,
+ fetchSingleConversation,
+ unloadSingleConversation,
+ sendMessage,
+ receiveMessage
+})(MessagesContainer)
diff --git a/src/client/containers/Settings/Tickets/index.jsx b/src/client/containers/Settings/Tickets/index.jsx
index 89fbf4e72..0e43b6572 100644
--- a/src/client/containers/Settings/Tickets/index.jsx
+++ b/src/client/containers/Settings/Tickets/index.jsx
@@ -125,6 +125,15 @@ class TicketsSettings extends React.Component {
})
}
+ onPlayNewTicketSoundChange (e) {
+ this.props.updateSetting({
+ name: 'playNewTicketSound:enable',
+ value: e.target.checked,
+ stateName: 'playNewTicketSound',
+ noSnackbar: true
+ })
+ }
+
showModal (e, modal, props) {
e.preventDefault()
this.props.showModal(modal, props)
@@ -277,6 +286,22 @@ class TicketsSettings extends React.Component {
/>
}
/>
+ {/* TODO: MOVE TO USER PREFS WHEN IMPL */}
+ {/*