diff --git a/src/client/actions/messages.js b/src/client/actions/messages.js new file mode 100644 index 000000000..45cc201dc --- /dev/null +++ b/src/client/actions/messages.js @@ -0,0 +1,59 @@ +/* + * . .o8 oooo + * .o8 "888 `888 + * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo + * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P' + * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888. + * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b. + * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o + * ======================================================================== + * Author: Chris Brame + * Updated: 7/2/22 5:23 AM + * Copyright (c) 2014-2022. Trudesk Inc (Chris Brame) All rights reserved. + */ + +import { createAction } from 'redux-actions' +import { + FETCH_CONVERSATIONS, + FETCH_SINGLE_CONVERSATION, + MESSAGES_SEND, + MESSAGES_UI_RECEIVE, + UNLOAD_SINGLE_CONVERSATION, + UNLOAD_CONVERSATIONS +} from 'actions/types' + +export const fetchConversations = createAction( + FETCH_CONVERSATIONS.ACTION, + payload => payload, + () => ({ thunk: true }) +) + +export const unloadConversations = createAction( + UNLOAD_CONVERSATIONS.ACTION, + () => {}, + () => ({ thunk: true }) +) + +export const fetchSingleConversation = createAction( + FETCH_SINGLE_CONVERSATION.ACTION, + payload => payload, + () => ({ thunk: true }) +) + +export const unloadSingleConversation = createAction( + UNLOAD_SINGLE_CONVERSATION.ACTION, + () => {}, + () => ({ thunk: true }) +) + +export const sendMessage = createAction( + MESSAGES_SEND.ACTION, + payload => payload, + () => ({ thunk: true }) +) + +export const receiveMessage = createAction( + MESSAGES_UI_RECEIVE.SUCCESS, + payload => payload, + () => ({ thunk: true }) +) diff --git a/src/client/actions/types.js b/src/client/actions/types.js index 9decb879d..a7486b0f4 100644 --- a/src/client/actions/types.js +++ b/src/client/actions/types.js @@ -95,6 +95,14 @@ export const UPDATE_DEPARTMENT = defineAction('UPDATE_DEPARTMENT', [SUCCESS, PEN export const DELETE_DEPARTMENT = defineAction('DELETE_DEPARTMENT', [SUCCESS, PENDING, ERROR]) export const UNLOAD_DEPARTMENTS = defineAction('UNLOAD_DEPARTMENTS', [SUCCESS]) +// Messages +export const FETCH_CONVERSATIONS = defineAction('FETCH_CONVERSATIONS', [SUCCESS, PENDING, ERROR]) +export const UNLOAD_CONVERSATIONS = defineAction('UNLOAD_CONVERSATIONS', [SUCCESS]) +export const FETCH_SINGLE_CONVERSATION = defineAction('FETCH_SINGLE_CONVERSATION', [SUCCESS, PENDING, ERROR]) +export const UNLOAD_SINGLE_CONVERSATION = defineAction('UNLOAD_SINGLE_CONVERSATION', [SUCCESS]) +export const MESSAGES_SEND = defineAction('MESSAGES_SEND', [SUCCESS, ERROR]) +export const MESSAGES_UI_RECEIVE = defineAction('MESSAGES_UI_RECEIVE', [SUCCESS]) + // Notices export const FETCH_NOTICES = defineAction('FETCH_NOTICES', [PENDING, SUCCESS, ERROR]) export const CREATE_NOTICE = defineAction('CREATE_NOTICE', [SUCCESS, PENDING, ERROR]) diff --git a/src/client/api/index.js b/src/client/api/index.js index 52c2e28f7..63a90a703 100644 --- a/src/client/api/index.js +++ b/src/client/api/index.js @@ -288,6 +288,23 @@ api.departments.delete = ({ _id }) => { }) } +api.messages = {} +api.messages.getConversations = payload => { + return axios.get('/api/v2/messages/conversations').then(res => { + return res.data + }) +} +api.messages.getSingleConversation = ({ _id }) => { + return axios.get(`/api/v2/messages/conversations/${_id}`).then(res => { + return res.data + }) +} +api.messages.send = payload => { + return axios.post('/api/v1/messages/send', payload).then(res => { + return res.data + }) +} + api.notices = {} api.notices.create = payload => { return axios.post('/api/v2/notices', payload).then(res => { diff --git a/src/client/components/PageTitle/index.jsx b/src/client/components/PageTitle/index.jsx index 793fbefed..b40ef2dbb 100644 --- a/src/client/components/PageTitle/index.jsx +++ b/src/client/components/PageTitle/index.jsx @@ -14,13 +14,23 @@ import React from 'react' import PropTypes from 'prop-types' +import clsx from 'clsx' class PageTitle extends React.Component { render () { - const { title, rightComponent, shadow } = this.props + const { title, rightComponent, shadow, hideBorderBottom, extraClasses } = this.props return ( -
-
+
+

{title}

{rightComponent}
@@ -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 ( +
+ + + +
+ {!this.userListShown && ( + this.showUserList(e)} + > + + add + + + )} + {this.userListShown && ( + this.hideUserList(e)} + > + Cancel + + )} +
+
+ } + /> + +
+
    + {this.props.messagesState.conversations.map(convo => { + const partnerImage = convo.get('partner').get('image') || 'defaultProfile.jpg' + const updatedDate = helpers.getCalendarDate(convo.get('updatedAt')) + const isCurrentConversation = !!( + this.props.messagesState.currentConversation && + this.props.messagesState.currentConversation.toJS()._id.toString() === convo.toJS()._id.toString() + ) + + return ( +
  • this.onConversationClicked(convo.get('_id'))} + > +
    + {convo.get('partner').get('fullname')} + +
    +
    + {convo.get('partner').get('fullname')} + {updatedDate} + {convo.get('recentMessage')} +
    +
  • + ) + })} +
+
+ + {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 && ( +
+ Profile Image +
{message.get('body')}
+
+ )} + {isMessageOwner && ( +
+
+ {message.get('body')} +
+
+ )} +
+ ) + })} + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ this.onSendMessageSubmit( + e, + currentConversation.get('_id'), + currentConversation.get('partner').get('_id') + ) + } + > + + this.onSendMessageKeyDown( + e, + currentConversation.get('_id'), + currentConversation.get('partner').get('_id') + ) + } + /> + +
+
+ + )} + +
    +
  • + 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 */} + {/* {*/} + {/* this.onPlayNewTicketSoundChange(e)*/} + {/* }}*/} + {/* />*/} + {/* }*/} + {/*/>*/} ({ tagsSettings: state.tagsSettings }) -export default connect( - mapStateToProps, - { updateSetting, getTagsWithPage, tagsUpdateCurrentPage, showModal } -)(TicketsSettings) +export default connect(mapStateToProps, { updateSetting, getTagsWithPage, tagsUpdateCurrentPage, showModal })( + TicketsSettings +) diff --git a/src/client/lib/socket/ticketSocketEvents.jsx b/src/client/lib/socket/ticketSocketEvents.jsx index bcaaa7cf2..24c8af3b8 100644 --- a/src/client/lib/socket/ticketSocketEvents.jsx +++ b/src/client/lib/socket/ticketSocketEvents.jsx @@ -5,17 +5,23 @@ import helpers from 'lib/helpers' const TicketSocketEvents = () => { const socket = useSelector(state => state.shared.socket) + const viewdata = useSelector(state => state.common.viewdata) useEffect(() => { if (socket) { ticketCreated() } - }, [socket]) + }, [socket, viewdata]) const ticketCreated = () => { socket.removeAllListeners(TICKETS_CREATED) socket.on(TICKETS_CREATED, ticket => { - helpers.UI.playSound('TICKET_CREATED') + if (viewdata) { + if (viewdata.get('ticketSettings') && viewdata.get('ticketSettings').get('playNewTicketSound')) + helpers.UI.playSound('TICKET_CREATED') + } else { + helpers.UI.playSound('TICKET_CREATED') + } }) } diff --git a/src/client/reducers/index.js b/src/client/reducers/index.js index 9a20841e7..96bff4f6b 100644 --- a/src/client/reducers/index.js +++ b/src/client/reducers/index.js @@ -28,6 +28,7 @@ import teamsState from './teamsReducer' import departmentsState from './departmentsReducer' import noticesState from './noticesReducer' import searchState from './searchReducer' +import messagesState from './messagesReducer' // const IndexReducer = (state = {}, action) => { // return { @@ -59,7 +60,8 @@ const IndexReducer = combineReducers({ departmentsState, noticesState, settings, - tagsSettings + tagsSettings, + messagesState }) export default IndexReducer diff --git a/src/client/reducers/messagesReducer.js b/src/client/reducers/messagesReducer.js new file mode 100644 index 000000000..8069ad347 --- /dev/null +++ b/src/client/reducers/messagesReducer.js @@ -0,0 +1,123 @@ +/* + * . .o8 oooo + * .o8 "888 `888 + * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo + * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P' + * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888. + * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b. + * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o + * ======================================================================== + * Author: Chris Brame + * Updated: 7/1/22 12:16 AM + * Copyright (c) 2014-2022. Trudesk, Inc (Chris Brame) All rights reserved. + */ + +import { fromJS, Map, List } from 'immutable' +import { handleActions } from 'redux-actions' +import { sortBy, map } from 'lodash' +import { + FETCH_CONVERSATIONS, + FETCH_SINGLE_CONVERSATION, + MESSAGES_SEND, + MESSAGES_UI_RECEIVE, + UNLOAD_SINGLE_CONVERSATION, + UNLOAD_CONVERSATIONS +} from 'actions/types' + +const initialState = { + loading: false, + conversations: List([]), + + loadingSingleConversation: false, + currentConversation: null +} + +const reducer = handleActions( + { + [FETCH_CONVERSATIONS.PENDING]: state => { + return { + ...state, + loading: true + } + }, + + [FETCH_CONVERSATIONS.SUCCESS]: (state, action) => { + return { + ...state, + loading: false, + conversations: fromJS(action.response.conversations) + } + }, + + [UNLOAD_CONVERSATIONS.SUCCESS]: state => { + return { + ...state, + conversations: initialState.conversations + } + }, + + [FETCH_SINGLE_CONVERSATION.PENDING]: state => { + return { + ...state, + loadingSingleConversation: true + } + }, + + [FETCH_SINGLE_CONVERSATION.SUCCESS]: (state, action) => { + return { + ...state, + loadingSingleConversation: false, + currentConversation: fromJS(action.response.conversation) + } + }, + + [UNLOAD_SINGLE_CONVERSATION.SUCCESS]: state => { + return { + ...state, + currentConversation: null + } + }, + + [MESSAGES_SEND.SUCCESS]: (state, action) => { + return { ...state } + }, + + [MESSAGES_UI_RECEIVE.SUCCESS]: (state, action) => { + const message = fromJS(action.payload.message) + const isOwner = action.payload.isOwner + + let conversation = state.conversations.find( + c => c.get('_id').toString() === message.get('conversation').toString() + ) + const index = state.conversations.indexOf(conversation) + + conversation = conversation.set( + 'recentMessage', + `${isOwner ? 'You' : message.get('owner').get('fullname')}: ${message.get('body')}` + ) + + conversation = conversation.set('updatedAt', message.get('createdAt')) + + if ( + !state.currentConversation || + state.currentConversation.get('_id').toString() !== message.get('conversation').toString() + ) { + return { + ...state, + conversations: state.conversations.set(index, conversation) + } + } + + const newMessageList = state.currentConversation.get('messages').push(message) + + return { + ...state, + conversations: state.conversations.set(index, conversation), + currentConversation: state.currentConversation.set('messages', newMessageList) + } + } + }, + initialState +) + +export default reducer diff --git a/src/client/renderer.jsx b/src/client/renderer.jsx index f7881fb2b..de33635af 100644 --- a/src/client/renderer.jsx +++ b/src/client/renderer.jsx @@ -27,6 +27,7 @@ import TeamsContainer from 'containers/Teams' import DepartmentsContainer from 'containers/Departments' import NoticeContainer from 'containers/Notice/NoticeContainer' import ProfileContainer from 'containers/Profile' +import MessagesContainer from 'containers/Messages' export default function (store) { if (document.getElementById('dashboard-container')) { @@ -128,6 +129,17 @@ export default function (store) { ReactDOM.render(TeamsContainerWithProvider, document.getElementById('departments-container')) } + if (document.getElementById('messages-container')) { + const conversation = document.getElementById('messages-container').getAttribute('data-conversation-id') + const MessagesContainterWithProvider = ( + + + + ) + + ReactDOM.render(MessagesContainterWithProvider, document.getElementById('messages-container')) + } + if (document.getElementById('notices-container')) { const NoticeContainerWithProvider = ( diff --git a/src/client/sagas/index.js b/src/client/sagas/index.js index fc600488c..7fe62b8aa 100644 --- a/src/client/sagas/index.js +++ b/src/client/sagas/index.js @@ -23,6 +23,7 @@ import TeamSaga from './teams' import DepartmentSaga from './departments' import NoticeSage from './notices' import SearchSaga from './search' +import MessagesSaga from './messages' export default function * IndexSagas () { yield all([ @@ -35,6 +36,7 @@ export default function * IndexSagas () { TeamSaga(), DepartmentSaga(), NoticeSage(), - SearchSaga() + SearchSaga(), + MessagesSaga() ]) } diff --git a/src/client/sagas/messages/index.js b/src/client/sagas/messages/index.js new file mode 100644 index 000000000..f64a5a5c0 --- /dev/null +++ b/src/client/sagas/messages/index.js @@ -0,0 +1,103 @@ +/* + * . .o8 oooo + * .o8 "888 `888 + * .o888oo oooo d8b oooo oooo .oooo888 .ooooo. .oooo.o 888 oooo + * 888 `888""8P `888 `888 d88' `888 d88' `88b d88( "8 888 .8P' + * 888 888 888 888 888 888 888ooo888 `"Y88b. 888888. + * 888 . 888 888 888 888 888 888 .o o. )88b 888 `88b. + * "888" d888b `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""888P' o888o o888o + * ======================================================================== + * Author: Chris Brame + * Updated: 7/2/22 5:23 AM + * Copyright (c) 2014-2022. Trudesk Inc (Chris Brame) All rights reserved. + */ + +import { call, put, takeLatest, takeEvery } from 'redux-saga/effects' + +import api from '../../api' +import { + FETCH_CONVERSATIONS, + FETCH_SINGLE_CONVERSATION, + UNLOAD_SINGLE_CONVERSATION, + MESSAGES_SEND, + UNLOAD_CONVERSATIONS +} from 'actions/types' + +import Log from '../../logger' +import helpers from 'lib/helpers' + +function * fetchConversations ({ payload }) { + yield put({ type: FETCH_CONVERSATIONS.PENDING }) + try { + const response = yield call(api.messages.getConversations, payload) + yield put({ type: FETCH_CONVERSATIONS.SUCCESS, response }) + } catch (error) { + const errorText = error.response ? error.response.data.error : error + if (error.response && error.response.status !== (401 || 403)) { + Log.error(errorText, error) + helpers.UI.showSnackbar(`Error: ${errorText}`, true) + } + + yield put({ type: FETCH_CONVERSATIONS.ERROR, error }) + } +} + +function * unloadConversations ({ meta }) { + try { + yield put({ type: UNLOAD_CONVERSATIONS.SUCCESS, meta }) + } catch (error) { + Log.error(error) + + yield put({ type: UNLOAD_CONVERSATIONS.ERROR, error }) + } +} + +function * fetchSingleConversation ({ payload, meta }) { + yield put({ type: FETCH_SINGLE_CONVERSATION.PENDING }) + try { + const response = yield call(api.messages.getSingleConversation, payload) + yield put({ type: FETCH_SINGLE_CONVERSATION.SUCCESS, response, meta }) + } catch (error) { + const errorText = error.response ? error.response.data.error : error + if (error.response && error.response.status !== (401 || 403)) { + Log.error(errorText, error) + helpers.UI.showSnackbar(`Error: ${errorText}`, true) + } + + yield put({ type: FETCH_SINGLE_CONVERSATION.ERROR, error }) + } +} + +function * unloadSingleConversation ({ meta }) { + try { + yield put({ type: UNLOAD_SINGLE_CONVERSATION.SUCCESS, meta }) + } catch (error) { + Log.error(error) + + yield put({ type: UNLOAD_SINGLE_CONVERSATION.ERROR, error }) + } +} + +function * sendMessage ({ payload, meta }) { + try { + const response = yield call(api.messages.send, payload) + yield put({ type: MESSAGES_SEND.SUCCESS, payload: response, response, meta }) + } catch (error) { + Log.error(error) + const errorText = error.response ? error.response.data.error : error + if (error.response && error.response.status !== (401 || 403)) { + Log.error(errorText, error) + helpers.UI.showSnackbar(`Error: ${errorText}`, true) + } + + yield put({ type: MESSAGES_SEND.ERROR, error }) + } +} + +export default function * watcher () { + yield takeLatest(FETCH_CONVERSATIONS.ACTION, fetchConversations) + yield takeLatest(UNLOAD_CONVERSATIONS.ACTION, unloadConversations) + yield takeLatest(FETCH_SINGLE_CONVERSATION.ACTION, fetchSingleConversation) + yield takeLatest(UNLOAD_SINGLE_CONVERSATION.ACTION, unloadSingleConversation) + yield takeEvery(MESSAGES_SEND.ACTION, sendMessage) +} diff --git a/src/controllers/api.js b/src/controllers/api.js index 715b58a30..7175e872f 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -52,5 +52,6 @@ apiController.v2.departments = require('./api/v2/departments') apiController.v2.notices = require('./api/v2/notices') apiController.v2.elasticsearch = require('./api/v2/elasticsearch') apiController.v2.mailer = require('./api/v2/mailer') +apiController.v2.messages = require('./api/v2/messages') module.exports = apiController diff --git a/src/controllers/api/v1/messages.js b/src/controllers/api/v1/messages.js index 4db4d20bb..3508e7126 100644 --- a/src/controllers/api/v1/messages.js +++ b/src/controllers/api/v1/messages.js @@ -217,7 +217,7 @@ apiMessages.send = function (req, res) { function (user, convo, done) { const Message = new MessageSchema({ conversation: convo._id, - owner: user._id, + owner: user, body: message }) diff --git a/src/controllers/api/v2/messages.js b/src/controllers/api/v2/messages.js index 2d1cd800a..2d5637165 100644 --- a/src/controllers/api/v2/messages.js +++ b/src/controllers/api/v2/messages.js @@ -11,3 +11,93 @@ * Updated: 2/14/19 12:05 AM * Copyright (c) 2014-2019. All rights reserved. */ + +const _ = require('lodash') +const apiUtils = require('../apiUtils') +const Conversation = require('../../../models/chat/conversation') +const Message = require('../../../models/chat/message') + +const apiMessages = {} + +apiMessages.getConversations = async (req, res) => { + try { + const resConversations = [] + const conversations = await Conversation.getConversationsWithLimit(req.user._id) + for (const convo of conversations) { + const convoObject = convo.toObject() + + const userMeta = + convo.userMeta[_.findIndex(convo.userMeta, item => item.userId.toString() === req.user._id.toString())] + + if (!_.isUndefined(userMeta) && !_.isUndefined(userMeta.deletedAt) && userMeta.deletedAt > convo.updatedAt) + continue + + let recentMessage = await Message.getMostRecentMessage(convoObject._id) + + for (const participant of convoObject.participants) { + if (participant._id.toString() !== req.user._id.toString()) { + convoObject.partner = participant + } + } + + recentMessage = _.first(recentMessage) + + if (!_.isUndefined(recentMessage)) { + if (convoObject.partner._id.toString() === recentMessage.owner._id.toString()) { + convoObject.recentMessage = `${convoObject.partner.fullname}: ${recentMessage.body}` + } else { + convoObject.recentMessage = `You: ${recentMessage.body}` + } + } else { + convoObject.recentMessage = 'New Conversation' + } + + resConversations.push(convoObject) + } + + return apiUtils.sendApiSuccess(res, { conversations: resConversations }) + } catch (e) { + return apiUtils.sendApiError(res, 500, e.message) + } +} + +apiMessages.single = async (req, res) => { + const _id = req.params.id + if (!_id) return apiUtils.sendApiError(res, 400, 'Invalid Conversation Id') + try { + let conversation = await Conversation.getConversation(_id) + + if (!conversation) return apiUtils.sendApiError(res, 404, 'Conversation not found') + + conversation = conversation.toObject() + let isPart = false + for (const participant of conversation.participants) { + if (participant._id.toString() === req.user._id.toString()) isPart = true + } + + if (!isPart) return apiUtils.sendApiError(res, 400, 'Invalid') + + const convoMessages = await Message.getConversationWithObject({ + cid: conversation._id, + userMeta: conversation.userMeta, + requestingUser: req.user + }) + + for (const participant of conversation.participants) { + if (participant._id.toString() !== req.user._id.toString()) conversation.partner = participant + } + + conversation.requestingUserMeta = + conversation.userMeta[ + _.findIndex(conversation.userMeta, item => item.userId.toString() === req.user._id.toString()) + ] + + conversation.messages = convoMessages.reverse() + + return apiUtils.sendApiSuccess(res, { conversation }) + } catch (e) { + return apiUtils.sendApiError(res, 500, e.message) + } +} + +module.exports = apiMessages diff --git a/src/controllers/api/v2/routes.js b/src/controllers/api/v2/routes.js index 226573381..ad671c6c7 100644 --- a/src/controllers/api/v2/routes.js +++ b/src/controllers/api/v2/routes.js @@ -78,6 +78,9 @@ module.exports = function (middleware, router, controllers) { router.get('/api/v2/notices/clear', apiv2Auth, canUser('notices:deactivate'), apiv2.notices.clear) router.delete('/api/v2/notices/:id', apiv2Auth, canUser('notices:delete'), apiv2.notices.delete) + router.get('/api/v2/messages/conversations', apiv2Auth, apiv2.messages.getConversations) + router.get('/api/v2/messages/conversations/:id', apiv2Auth, apiv2.messages.single) + // ElasticSearch router.get('/api/v2/es/search', middleware.api, apiv2.elasticsearch.search) router.get('/api/v2/es/rebuild', apiv2Auth, isAdmin, apiv2.elasticsearch.rebuild) diff --git a/src/controllers/messages.js b/src/controllers/messages.js index 7c8d3640f..bf71b2b5e 100644 --- a/src/controllers/messages.js +++ b/src/controllers/messages.js @@ -21,6 +21,17 @@ const messagesController = {} messagesController.content = {} +messagesController.view = (req, res) => { + const content = {} + content.title = 'Messages' + content.nav = 'messages' + content.data = {} + content.data.common = req.viewdata + if (req.params.convoid) content.data.conversationId = req.params.convoid + + return res.render('messages', content) +} + messagesController.get = function (req, res) { const content = {} content.title = 'Messages' diff --git a/src/helpers/viewdata/index.js b/src/helpers/viewdata/index.js index 33516e825..2d92b393a 100644 --- a/src/helpers/viewdata/index.js +++ b/src/helpers/viewdata/index.js @@ -77,6 +77,17 @@ viewController.getData = function (request, cb) { viewdata.ticketSettings = {} async.parallel( [ + function (done) { + settingSchema.getSetting('playNewTicketSound:enable', function (err, setting) { + if (!err && setting && !_.isUndefined(setting.value)) { + viewdata.ticketSettings.playNewTicketSound = setting.value + } else { + viewdata.ticketSettings.playNewTicketSound = true + } + }) + + return done() + }, function (done) { settingSchema.getSetting('ticket:minlength:subject', function (err, setting) { if (!err && setting && setting.value) { diff --git a/src/models/chat/conversation.js b/src/models/chat/conversation.js index 07f448272..f2fbc0b8f 100644 --- a/src/models/chat/conversation.js +++ b/src/models/chat/conversation.js @@ -69,27 +69,59 @@ conversationSchema.statics.getConversations = function (userId, callback) { } conversationSchema.statics.getConversation = function (convoId, callback) { - return this.model(COLLECTION) - .findOne({ _id: convoId }) - .populate({ - path: 'participants', - select: '_id username fullname email title image lastOnline' - }) - .exec(callback) + const self = this + return new Promise((resolve, reject) => { + ;(async () => { + try { + const query = self + .model(COLLECTION) + .findOne({ _id: convoId }) + .populate({ + path: 'participants', + select: '_id username fullname email title image lastOnline' + }) + + if (typeof callback === 'function') return query.exec(callback) + + const results = await query.exec() + + return resolve(results) + } catch (e) { + if (typeof callback === 'function') return callback(e) + return reject(e) + } + })() + }) } conversationSchema.statics.getConversationsWithLimit = function (userId, limit, callback) { - // if (!_.isArray(userId)) userId = [userId]; - var l = !_.isUndefined(limit) ? limit : 1000 - return this.model(COLLECTION) - .find({ participants: userId }) - .sort('-updatedAt') - .limit(l) - .populate({ - path: 'participants', - select: 'username fullname email title image lastOnline' - }) - .exec(callback) + const self = this + return new Promise((resolve, reject) => { + ;(async () => { + try { + const l = limit || 1000 + const query = self + .model(COLLECTION) + .find({ participants: userId }) + .sort('-updatedAt') + .limit(l) + .populate({ + path: 'participants', + select: 'username fullname email title image lastOnline' + }) + + if (typeof callback === 'function') return query.exec(callback) + + const results = await query.exec() + + return resolve(results) + } catch (e) { + if (typeof callback === 'function') return callback(e) + + return reject(e) + } + })() + }) } module.exports = mongoose.model(COLLECTION, conversationSchema) diff --git a/src/models/chat/message.js b/src/models/chat/message.js index e788bc8d7..fb5964ed8 100644 --- a/src/models/chat/message.js +++ b/src/models/chat/message.js @@ -68,60 +68,85 @@ messageSchema.statics.getConversation = function (convoId, callback) { } messageSchema.statics.getConversationWithObject = function (object, callback) { - if (!_.isObject(object)) { - return callback('Invalid Object (Must by of type Object) - MessageSchema.GetUserWithObject()', null) - } - const self = this - let deletedAt = null - const limit = object.limit === null ? 25 : object.limit const page = object.page === null ? 0 : object.page - if (object.requestingUser) { - const userMetaIdx = _.findIndex(object.userMeta, function (item) { - return item.userId.toString() === object.requestingUser._id.toString() - }) - if (userMetaIdx !== -1 && object.userMeta[userMetaIdx].deletedAt) { - deletedAt = new Date(object.userMeta[userMetaIdx].deletedAt) - } - } - - const q = self - .model(COLLECTION) - .find({}) - .sort('-createdAt') - .skip(page * limit) - .populate({ - path: 'owner', - select: '_id username fullname email image lastOnline' - }) - - if (limit !== -1) { - q.limit(limit) - } - - if (object.cid !== null) { - q.where({ conversation: object.cid }) - } - - if (deletedAt) { - q.where({ createdAt: { $gte: deletedAt } }) - } - - return q.exec(callback) + return new Promise((resolve, reject) => { + ;(async () => { + try { + if (!_.isObject(object)) { + if (typeof callback === 'function') + return callback('Invalid Object (Must by of type Object) - MessageSchema.GetUserWithObject()') + + return reject(new Error('Invalid Object (Must by of type Object) - MessageSchema.GetUserWithObject()')) + } + + let deletedAt = null + + if (object.requestingUser) { + const userMetaIdx = _.findIndex(object.userMeta, item => { + return item.userId.toString() === object.requestingUser._id.toString() + }) + if (userMetaIdx !== -1 && object.userMeta[userMetaIdx].deletedAt) + deletedAt = new Date(object.userMeta[userMetaIdx].deletedAt) + } + + const query = self + .model(COLLECTION) + .find({}) + .sort('-createdAt') + .skip(page * limit) + .populate({ + path: 'owner', + select: '_id username fullname email image lastOnline' + }) + + if (limit !== -1) query.limit(limit) + if (object.cid) query.where({ conversation: object.cid }) + if (deletedAt) query.where({ createdAt: { $gte: deletedAt } }) + + if (typeof callback === 'function') return query.exec(callback) + + const results = await query.exec() + + return resolve(results) + } catch (e) { + if (typeof callback === 'function') return callback(e) + + return reject(e) + } + })() + }) } messageSchema.statics.getMostRecentMessage = function (convoId, callback) { - return this.model(COLLECTION) - .find({ conversation: convoId }) - .sort('-createdAt') - .limit(1) - .populate({ - path: 'owner', - select: '_id username fullname image lastOnline' - }) - .exec(callback) + const self = this + return new Promise((resolve, reject) => { + ;(async () => { + try { + const query = self + .model(COLLECTION) + .find({ conversation: convoId }) + .sort('-createdAt') + .limit(1) + .populate({ + path: 'owner', + select: '_id username fullname image lastOnline' + }) + + if (typeof callback === 'function') return query.exec(callback) + + const results = await query.exec() + + return resolve(results) + } catch (e) { + if (typeof callback === 'function') return callback(e) + + return reject(e) + } + })() + }) } module.exports = mongoose.model(COLLECTION, messageSchema) diff --git a/src/public/js/modules/ajaxify.js b/src/public/js/modules/ajaxify.js index 7681e349c..66c3a5b39 100644 --- a/src/public/js/modules/ajaxify.js +++ b/src/public/js/modules/ajaxify.js @@ -52,7 +52,7 @@ define('modules/ajaxify', [ helpers.UI.cardShow() helpers.countUpMe() - var event = _.debounce(function () { + const event = _.debounce(function () { $.event.trigger('$trudesk:ready') }, 100) @@ -60,9 +60,9 @@ define('modules/ajaxify', [ }) // Prepare our Variables - var History = window.History + const History = window.History - var document = window.document + const document = window.document // Check to see if History.js is enabled for our Browser if (!History.enabled) { @@ -250,6 +250,8 @@ define('modules/ajaxify', [ window.react.dom.unmountComponentAtNode(document.getElementById('departments-container')) if (document.getElementById('notices-container')) window.react.dom.unmountComponentAtNode(document.getElementById('notices-container')) + if (document.getElementById('messages-container')) + window.react.dom.unmountComponentAtNode(document.getElementById('messages-container')) // if (document.getElementById('modal-wrapper')) // window.react.dom.unmountComponentAtNode(document.getElementById('modal-wrapper')) diff --git a/src/public/js/modules/chat.js b/src/public/js/modules/chat.js index a80195064..6311633eb 100644 --- a/src/public/js/modules/chat.js +++ b/src/public/js/modules/chat.js @@ -19,11 +19,9 @@ define('modules/chat', ['jquery', 'underscore', 'moment', 'modules/helpers', 'ui helpers, UIKit ) { - var chatClient = {} - - var socket - - var loggedInAccount + const chatClient = {} + let socket + let loggedInAccount chatClient.init = function (sock) { loggedInAccount = window.trudeskSessionService.getUser() @@ -126,24 +124,17 @@ define('modules/chat', ['jquery', 'underscore', 'moment', 'modules/helpers', 'ui } }) - socket.removeAllListeners('chatMessage') - socket.on('chatMessage', function (data) { - var type = data.type - var to = data.to - var from = data.from - var chatBox = '' - - var chatMessage = '' - - var chatMessageList = '' - - var scroller = '' - - var selector = '' + socket.removeAllListeners('$trudesk:messages:ui:receive') + socket.on('$trudesk:messages:ui:receive', function (data) { + console.log(data) + const type = data.message.owner._id.toString() === loggedInAccount._id.toString() ? 's' : 'r' + const to = data.to + const from = data.from + let chatBox, chatMessage, chatMessageList, scroller, selector if (type === 's') { chatBox = $('.chat-box[data-chat-userid="' + to + '"]') - chatMessage = createChatMessageDiv(data.message) + chatMessage = createChatMessageDiv(data.message.body) chatMessageList = chatBox.find('.chat-message-list:first') scroller = chatBox.find('.chat-box-messages') chatMessageList.append(chatMessage) @@ -159,7 +150,7 @@ define('modules/chat', ['jquery', 'underscore', 'moment', 'modules/helpers', 'ui helpers.scrollToBottom(scroller) }) } else { - chatMessage = createChatMessageFromUser(data.fromUser, data.message) + chatMessage = createChatMessageFromUser(data.fromUser, data.message.body) chatMessageList = chatBox.find('.chat-message-list:first') chatMessageList.append(chatMessage) scroller = chatBox.find('.chat-box-messages') @@ -175,8 +166,8 @@ define('modules/chat', ['jquery', 'underscore', 'moment', 'modules/helpers', 'ui $('body').ajaxify() }) - socket.removeAllListeners('chatTyping') - socket.on('chatTyping', function (data) { + socket.removeAllListeners('$trudesk:messages:ui:user_typing') + socket.on('$trudesk:messages:ui:user_typing', function (data) { $.event.trigger('$trudesk:chat:typing', data) var chatBox = $('div[data-conversation-id="' + data.cid + '"]') var isTypingDiv = chatBox.find('.user-is-typing-wrapper') diff --git a/src/public/js/modules/helpers.js b/src/public/js/modules/helpers.js index 913a7b616..13cfc5540 100644 --- a/src/public/js/modules/helpers.js +++ b/src/public/js/modules/helpers.js @@ -1524,6 +1524,18 @@ define([ return 'MMM DD, YYYY' } + helpers.getTimeFormat = function () { + if (window.trudeskSettingsService) { + return window.trudeskSettingsService.getSettings().timeFormat.value + } + + return 'hh:mma' + } + + helpers.getShortDateWithTimeFormat = function () { + return `${helpers.getShortDateFormat()} ${helpers.getTimeFormat()}` + } + helpers.formatDate = function (date, format) { var timezone = this.getTimezone() if (!timezone) { diff --git a/src/routes/index.js b/src/routes/index.js index 38f82752f..914224be0 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -223,12 +223,7 @@ function mainRoutes (router, middleware, controllers) { }, controllers.messages.get ) - router.get( - '/messages/:convoid', - middleware.redirectToLogin, - middleware.loadCommonData, - controllers.messages.getConversation - ) + router.get('/messages/:convoid', middleware.redirectToLogin, middleware.loadCommonData, controllers.messages.view) // Accounts router.get( diff --git a/src/sass/partials/messages.sass b/src/sass/partials/messages.sass index 481244492..9e2c84428 100644 --- a/src/sass/partials/messages.sass +++ b/src/sass/partials/messages.sass @@ -305,10 +305,11 @@ input[type="checkbox"]:checked.poloCheckbox + label &.message-right float: right .message-body - background: $accent_color + background: $accent_blue margin-left: 0 margin-right: 10px float: right + color: #fff p color: #fff diff --git a/src/settings/settingsUtil.js b/src/settings/settingsUtil.js index b42728c12..640c0a14f 100644 --- a/src/settings/settingsUtil.js +++ b/src/settings/settingsUtil.js @@ -116,6 +116,7 @@ util.getSettings = async callback => { s.allowAgentUserTickets = parseSetting(settings, 'allowAgentUserTickets:enable', false) s.allowPublicTickets = parseSetting(settings, 'allowPublicTickets:enable', false) s.allowUserRegistration = parseSetting(settings, 'allowUserRegistration:enable', false) + s.playNewTicketSound = parseSetting(settings, 'playNewTicketSound:enable', true) s.privacyPolicy = parseSetting(settings, 'legal:privacypolicy', '') s.privacyPolicy.value = jsStringEscape(s.privacyPolicy.value) diff --git a/src/socketio/chatSocket.js b/src/socketio/chatSocket.js index 5557ea0f8..862deddf5 100644 --- a/src/socketio/chatSocket.js +++ b/src/socketio/chatSocket.js @@ -19,6 +19,7 @@ var userSchema = require('../models/user') var sharedVars = require('./index').shared var sharedUtils = require('./index').utils +const socketEventConst = require('./socketEventConsts') var events = {} @@ -350,22 +351,16 @@ events.saveChatWindow = function (socket) { } events.onChatMessage = function (socket) { - socket.on('chatMessage', function (data) { - var to = data.to - var from = data.from - var od = data.type - if (data.type === 's') { - data.type = 'r' - } else { - data.type = 's' - } + socket.on(socketEventConst.MESSAGES_SEND, function (data) { + const to = data.to + const from = data.from - var userSchema = require('../models/user') + const User = require('../models/user') async.parallel( [ function (next) { - userSchema.getUser(to, function (err, toUser) { + User.getUser(to, function (err, toUser) { if (err) return next(err) if (!toUser) return next('User Not Found!') @@ -375,7 +370,7 @@ events.onChatMessage = function (socket) { }) }, function (next) { - userSchema.getUser(from, function (err, fromUser) { + User.getUser(from, function (err, fromUser) { if (err) return next(err) if (!fromUser) return next('User Not Found') @@ -386,23 +381,36 @@ events.onChatMessage = function (socket) { } ], function (err) { - if (err) return utils.sendToSelf(socket, 'chatMessage', { message: err }) - - utils.sendToUser(sharedVars.sockets, sharedVars.usersOnline, data.toUser.username, 'chatMessage', data) - data.type = od - utils.sendToUser(sharedVars.sockets, sharedVars.usersOnline, data.fromUser.username, 'chatMessage', data) + if (err) return utils.sendToSelf(socket, socketEventConst.MESSAGES_UI_RECEIVE, { message: err }) + + console.log(data) + utils.sendToUser( + sharedVars.sockets, + sharedVars.usersOnline, + data.toUser.username, + socketEventConst.MESSAGES_UI_RECEIVE, + data + ) + + utils.sendToUser( + sharedVars.sockets, + sharedVars.usersOnline, + data.fromUser.username, + socketEventConst.MESSAGES_UI_RECEIVE, + data + ) } ) }) } events.onChatTyping = function (socket) { - socket.on('chatTyping', function (data) { - var to = data.to - var from = data.from + socket.on(socketEventConst.MESSAGES_USER_TYPING, function (data) { + const to = data.to + const from = data.from - var user = null - var fromUser = null + let user = null + let fromUser = null _.find(sharedVars.usersOnline, function (v) { if (String(v.user._id) === String(to)) { @@ -421,14 +429,20 @@ events.onChatTyping = function (socket) { data.toUser = user data.fromUser = fromUser - utils.sendToUser(sharedVars.sockets, sharedVars.usersOnline, user.username, 'chatTyping', data) + utils.sendToUser( + sharedVars.sockets, + sharedVars.usersOnline, + user.username, + socketEventConst.MESSAGES_UI_USER_TYPING, + data + ) }) } events.onChatStopTyping = function (socket) { - socket.on('chatStopTyping', function (data) { - var to = data.to - var user = null + socket.on(socketEventConst.MESSAGES_USER_STOP_TYPING, function (data) { + const to = data.to + let user = null _.find(sharedVars.usersOnline, function (v) { if (String(v.user._id) === String(to)) { @@ -442,7 +456,13 @@ events.onChatStopTyping = function (socket) { data.toUser = user - utils.sendToUser(sharedVars.sockets, sharedVars.usersOnline, user.username, 'chatStopTyping', data) + utils.sendToUser( + sharedVars.sockets, + sharedVars.usersOnline, + user.username, + socketEventConst.MESSAGES_UI_USER_STOP_TYPING, + data + ) }) } diff --git a/src/socketio/socketEventConsts.js b/src/socketio/socketEventConsts.js index d76c566cb..0d0afe77d 100644 --- a/src/socketio/socketEventConsts.js +++ b/src/socketio/socketEventConsts.js @@ -62,4 +62,10 @@ exported.NOTIFICATIONS_UPDATE = '$trudesk:notifications:update' exported.NOTIFICATIONS_MARK_READ = '$trudesk:notifications:mark_read' exported.NOTIFICATIONS_CLEAR = '$trudesk:notifications:clear' +// MESSAGES +exported.MESSAGES_SEND = '$trudesk:messages:send' +exported.MESSAGES_UI_RECEIVE = '$trudesk:messages:ui:receive' +exported.MESSAGES_USER_TYPING = '$trudesk:messages:user_typing' +exported.MESSAGES_UI_USER_TYPING = '$trudesk:messages:ui:user_typing' + module.exports = exported diff --git a/src/views/dashboard.hbs b/src/views/dashboard.hbs index 0b8a78a2d..df5b60ab3 100644 --- a/src/views/dashboard.hbs +++ b/src/views/dashboard.hbs @@ -1,213 +1 @@ -
-
-

{{title}}

- -
-
- -
-
- -
- Last Updated: - Cache Still Loading... -
-
- -
-
-
-
-
-
-
- 5,3,9,6,5,9,7 -
- Total Tickets (last 30d) - -

- -- -

-
- -
-
-
-
-
-
- 0/100 -
- Tickets Completed - -

- --% -

-
-
-
-
-
-
-
- 5,3,9,6,5,9,7,3,5,2 -
- Avg Response Time - -

- -- hours -

-
-
-
-
- -
-
-
-
Ticket Breakdown
-
-
- - - - - - - - - - - - - -
-
-
-
- -
-
-
-
- -
-
-
-
Top 5 Groups
-
-
-
-
-
-
-
-
-
-
Top 10 Tags
-
-
-
-
-
-
-
-
-
-
- -
-
{{data.common.showOverdue}}
- {{#if data.common.showOverdue}} -
-
-
-
-
Overdue Tickets
-
-
-
-
-
-
- - - - - - - - - - - - -
TicketStatusSubjectLast Updated
-
-
-
-
-
-
- {{/if}} -
-
-
-
-
Quick Stats (Last 365 Days)
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
StatValue
Most tickets by...--
Most comments by....--
Most assigned support user....--
Most active ticket...--
-
-
-
-
-
-
-
- -
-
-
- -{{#contentFor 'js-plugins'}} - -{{/contentFor}} \ No newline at end of file +
\ No newline at end of file diff --git a/src/views/messages.hbs b/src/views/messages.hbs index 909dc68c0..0b22dbebe 100644 --- a/src/views/messages.hbs +++ b/src/views/messages.hbs @@ -1,180 +1 @@ -
-
{{data.showNewConvo}}
-
messages
-
-
-
-

Conversations

-
- add - -
-
-
- -
-
    - {{#foreach data.conversations}} -
  • -
    - {{#if partner.image}} - - {{else}} - - {{/if}} - -
    -
    - {{partner.fullname}} - {{calendarDate updatedAt}} - {{recentMessage}} -
    -
  • - {{/foreach}} -
-
- - - -
- {{#if data.conversation}} -
-
- Conversation Started on {{formatDate data.conversation.createdAt (concat data.common.longDateFormat data.common.timeFormat true true)}} - {{#if data.conversation.requestingUserMeta.deletedAt}} - Conversation Deleted at {{formatDate data.conversation.requestingUserMeta.deletedAt (concat data.common.longDateFormat data.common.timeFormat true true)}} - {{/if}} -
- -
-
- {{#each data.conversation.messages}} - {{#isNotAsString owner._id ../data.common.loggedInAccount._id}} -
- {{#if owner.image}} - - {{else}} - - {{/if}} -
-

{{{body}}}

-
-
- {{else}} -
-
-

{{{body}}}

-
-
- {{/isNotAsString}} - {{/each}} -
- -
- -
-
-
-
-
-
-
- -
-
- - -
-
-
- {{/if}} - -
-
    -
  • Delete Conversation
  • -
-
-
- -{{#contentFor 'js-plugins'}} - -{{/contentFor}} \ No newline at end of file +
\ No newline at end of file