New inbox client (#10644)

* new inbox client

* add tests for sendPrivateMessage returning the message

* update DELETE user message tests

* port v3 GET-inbox_messages

* use v4 delete message route

* sendPrivateMessage: return sent message

* fix
This commit is contained in:
Matteo Pagliazzi
2018-08-30 21:50:03 +02:00
committed by Sabe Jones
parent 64507a161e
commit 84329e5fad
14 changed files with 287 additions and 99 deletions

View File

@@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
} from '../../../../helpers/api-integration/v3';
describe('GET /inbox/messages', () => {
let user;
@@ -22,17 +22,26 @@ describe('GET /inbox/messages', () => {
message: 'third',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
await user.sync();
});
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages.length).to.equal(Object.keys(user.inbox.messages).length);
expect(messages.length).to.equal(4);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
// message to yourself
expect(messages[0].text).to.equal('fourth');
expect(messages[0].uuid).to.equal(user._id);
expect(messages[1].text).to.equal('third');
expect(messages[2].text).to.equal('second');
expect(messages[3].text).to.equal('first');
});
});
});

View File

@@ -100,7 +100,7 @@ describe('POST /members/send-private-message', () => {
let receiver = await generateUser();
// const initialNotifications = receiver.notifications.length;
await userToSendMessage.post('/members/send-private-message', {
const response = await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
@@ -116,6 +116,9 @@ describe('POST /members/send-private-message', () => {
return message.uuid === receiver._id && message.text === messageToSend;
});
expect(response.message.text).to.deep.equal(sendersMessageInSendersInbox.text);
expect(response.message.uuid).to.deep.equal(sendersMessageInSendersInbox.uuid);
// @TODO waiting for mobile support
// expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1);
// const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];

View File

@@ -3,25 +3,41 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('DELETE user message', () => {
let user;
let user, messagesId, otherUser;
beforeEach(async () => {
user = await generateUser({ inbox: { messages: { first: 'message', second: 'message' } } });
expect(user.inbox.messages.first).to.eql('message');
expect(user.inbox.messages.second).to.eql('message');
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
let userRes = await user.get('/user');
messagesId = Object.keys(userRes.inbox.messages);
expect(messagesId.length).to.eql(2);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('first');
expect(userRes.inbox.messages[messagesId[1]].text).to.eql('second');
});
it('one message', async () => {
let result = await user.del('/user/messages/first');
await user.sync();
expect(result).to.eql({ second: 'message' });
expect(user.inbox.messages).to.eql({ second: 'message' });
let result = await user.del(`/user/messages/${messagesId[0]}`);
messagesId = Object.keys(result);
expect(messagesId.length).to.eql(1);
let userRes = await user.get('/user');
expect(Object.keys(userRes.inbox.messages).length).to.eql(1);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('second');
});
it('clear all', async () => {
let result = await user.del('/user/messages');
await user.sync();
expect(user.inbox.messages).to.eql({});
let userRes = await user.get('/user');
expect(userRes.inbox.messages).to.eql({});
expect(result).to.eql({});
});
});

View File

@@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /inbox/messages/:messageId', () => {
let user;
let otherUser;
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
});
it('returns an error if the messageId parameter is not an UUID', async () => {
await expect(user.del('/inbox/messages/123'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns an error if the message does not exist', async () => {
await expect(user.del(`/inbox/messages/${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
it('deletes one message', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
await user.del(`/inbox/messages/${messages[1]._id}`);
const updatedMessages = await user.get('/inbox/messages');
expect(updatedMessages.length).to.equal(2);
expect(updatedMessages[0].text).to.equal('third');
expect(updatedMessages[1].text).to.equal('first');
});
});

View File

@@ -255,8 +255,7 @@ export default {
this.$emit('message-removed', message);
if (this.inbox) {
axios.delete(`/api/v4/user/messages/${message.id}`);
this.$delete(this.user.inbox.messages, message.id);
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
return;
}

View File

@@ -230,6 +230,11 @@ export default {
this.chat.splice(chatIndex, 1, message);
},
messageRemoved (message) {
if (this.inbox) {
this.$emit('message-removed', message);
return;
}
const chatIndex = findIndex(this.chat, chatMessage => {
return chatMessage.id === message.id;
});

View File

@@ -478,11 +478,6 @@ export default {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId;
});
},
deleteAllMessages () {
if (confirm(this.$t('confirmDeleteAllMessages'))) {
// User.clearPMs();
}
},
checkForAchievements () {
// Checks if user's party has reached 2 players for the first time.
if (!this.user.achievements.partyUp && this.group.memberCount >= 2) {

View File

@@ -1,5 +1,5 @@
<template lang="pug">
b-modal#inbox-modal(title="", :hide-footer="true", size='lg')
b-modal#inbox-modal(title="", :hide-footer="true", size='lg', @shown="onModalShown", @hide="onModalHide")
.header-wrap.container.align-items-center(slot="modal-header")
.row.align-items-center
.col-4
@@ -34,13 +34,19 @@
span.timeago {{conversation.date | timeAgo}}
div {{conversation.lastMessageText ? conversation.lastMessageText.substring(0, 30) : ''}}
.col-8.messages.d-flex.flex-column.justify-content-between
.empty-messages.text-center(v-if='activeChat.length === 0 && !selectedConversation.key')
.empty-messages.text-center(v-if='!selectedConversation.key')
.svg-icon.envelope(v-html="icons.messageIcon")
h4 {{placeholderTexts.title}}
p(v-html="placeholderTexts.description")
.empty-messages.text-center(v-if='activeChat.length === 0 && selectedConversation.key')
.empty-messages.text-center(v-if='selectedConversation.key && selectedConversationMessages.length === 0')
p {{ $t('beginningOfConversation', {userName: selectedConversation.name})}}
chat-message.message-scroll(v-if="activeChat.length > 0", :chat.sync='activeChat', :inbox='true', ref="chatscroll")
chat-messages.message-scroll(
v-if="selectedConversation.messages && selectedConversationMessages.length > 0",
:chat='selectedConversationMessages',
:inbox='true',
@message-removed='messageRemoved',
ref="chatscroll"
)
.pm-disabled-caption.text-center(v-if="user.inbox.optOut && selectedConversation.key")
h4 {{$t('PMDisabledCaptionTitle')}}
p {{$t('PMDisabledCaptionText')}}
@@ -196,43 +202,46 @@ import moment from 'moment';
import filter from 'lodash/filter';
import sortBy from 'lodash/sortBy';
import groupBy from 'lodash/groupBy';
import findIndex from 'lodash/findIndex';
import { mapState } from 'client/libs/store';
import styleHelper from 'client/mixins/styleHelper';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import axios from 'axios';
import messageIcon from 'assets/svg/message.svg';
import chatMessage from '../chat/chatMessages';
import chatMessages from '../chat/chatMessages';
import svgClose from 'assets/svg/close.svg';
export default {
mixins: [styleHelper],
components: {
chatMessage,
chatMessages,
toggleSwitch,
},
mounted () {
this.$root.$on('habitica::new-inbox-message', (data) => {
this.$root.$emit('bv::show::modal', 'inbox-modal');
const conversation = this.conversations.find(convo => {
return convo.key === data.userIdToMessage;
});
// Wait for messages to be loaded
const unwatchLoaded = this.$watch('loaded', (loaded) => {
if (!loaded) return;
const conversation = this.conversations.find(convo => {
return convo.key === data.userIdToMessage;
});
if (loaded) setImmediate(() => unwatchLoaded());
if (conversation) {
this.selectConversation(data.userIdToMessage);
return;
}
this.initiatedConversation = {
user: data.userName,
uuid: data.userIdToMessage,
};
if (conversation) {
this.selectConversation(data.userIdToMessage);
return;
}
const newMessage = {
text: '',
timestamp: new Date(),
user: data.userName,
uuid: data.userIdToMessage,
id: '',
};
this.$set(this.user.inbox.messages, data.userIdToMessage, newMessage);
this.selectConversation(data.userIdToMessage);
}, {immediate: true});
});
},
destroyed () {
@@ -248,8 +257,10 @@ export default {
selectedConversation: {},
search: '',
newMessage: '',
activeChat: [],
showPopover: false,
messages: [],
loaded: false,
initiatedConversation: null,
};
},
filters: {
@@ -260,8 +271,18 @@ export default {
computed: {
...mapState({user: 'user.data'}),
conversations () {
const inboxGroup = groupBy(this.user.inbox.messages, 'uuid');
const inboxGroup = groupBy(this.messages, 'uuid');
// Add placeholder for new conversations
if (this.initiatedConversation && this.initiatedConversation.uuid) {
inboxGroup[this.initiatedConversation.uuid] = [{
id: '',
text: '',
user: this.initiatedConversation.user,
uuid: this.initiatedConversation.uuid,
timestamp: new Date(),
}];
}
// Create conversation objects
const convos = [];
for (let key in inboxGroup) {
@@ -283,12 +304,9 @@ export default {
return newChat;
});
// In case the last message is a placeholder, remove it
const recentMessage = newChatModels[newChatModels.length - 1];
// Special case where we have placeholder message because conversations are just grouped messages for now
if (!recentMessage.text) {
newChatModels.splice(newChatModels.length - 1, 1);
}
if (!recentMessage.text) newChatModels.splice(newChatModels.length - 1, 1);
const convoModel = {
name: recentMessage.toUser ? recentMessage.toUser : recentMessage.user, // Handles case where from user sent the only message or the to user sent the only message
@@ -308,6 +326,12 @@ export default {
return conversations.reverse();
},
// Separate from selectedConversation which is not coputed so messages don't update automatically
selectedConversationMessages () {
const selectedConversationKey = this.selectedConversation.key;
const selectedConversation = this.conversations.find(c => c.key === selectedConversationKey);
return selectedConversation ? selectedConversation.messages : [];
},
filtersConversations () {
if (!this.search) return this.conversations;
return filter(this.conversations, (conversation) => {
@@ -343,6 +367,25 @@ export default {
},
},
methods: {
async onModalShown () {
this.loaded = false;
const res = await axios.get('/api/v4/inbox/messages');
this.messages = res.data.data;
this.loaded = true;
},
onModalHide () {
this.messages = [];
this.loaded = false;
this.initiatedConversation = null;
},
messageRemoved (message) {
const messageIndex = this.messages.findIndex(msg => msg.id === message.id);
if (messageIndex !== -1) this.messages.splice(messageIndex, 1);
if (this.selectedConversationMessages.length === 0) this.initiatedConversation = {
user: this.selectedConversation.name,
uuid: this.selectedConversation.key,
};
},
toggleClick () {
this.displayCreate = !this.displayCreate;
},
@@ -354,14 +397,7 @@ export default {
return conversation.key === key;
});
this.selectedConversation = convoFound;
let activeChat = convoFound.messages;
activeChat = sortBy(activeChat, [(o) => {
return moment(o.timestamp).toDate();
}]);
this.$set(this, 'activeChat', activeChat);
this.selectedConversation = convoFound || {};
Vue.nextTick(() => {
if (!this.$refs.chatscroll) return;
@@ -372,22 +408,22 @@ export default {
sendPrivateMessage () {
if (!this.newMessage) return;
const convoFound = this.conversations.find((conversation) => {
return conversation.key === this.selectedConversation.key;
});
convoFound.messages.push({
this.messages.push({
sent: true,
text: this.newMessage,
timestamp: new Date(),
user: this.user.profile.name,
uuid: this.user._id,
user: this.selectedConversation.name,
uuid: this.selectedConversation.key,
contributor: this.user.contributor,
});
this.activeChat = convoFound.messages;
// Remove the placeholder message
if (this.initiatedConversation && this.initiatedConversation.uuid === this.selectedConversation.key) {
this.initiatedConversation = null;
}
convoFound.lastMessageText = this.newMessage;
convoFound.date = new Date();
this.selectedConversation.lastMessageText = this.newMessage;
this.selectedConversation.date = new Date();
Vue.nextTick(() => {
if (!this.$refs.chatscroll) return;
@@ -400,8 +436,7 @@ export default {
message: this.newMessage,
}).then(response => {
const newMessage = response.data.data.message;
const messageIndex = findIndex(convoFound.messages, msg => !msg.id);
convoFound.messages.splice(convoFound.messages.length - 1, messageIndex, newMessage);
Object.assign(this.messages[this.messages.length - 1], newMessage);
});
this.newMessage = '';

View File

@@ -22,4 +22,6 @@ module.exports = {
missingCustomerId: 'Missing "req.query.customerId"',
missingPaypalBlock: 'Missing "req.session.paypalBlock"',
missingSubKey: 'Missing "req.query.sub"',
messageIdRequired: '\"messageId\" must be a valid UUID.",',
};

View File

@@ -0,0 +1,29 @@
import { authWithHeaders } from '../../middlewares/auth';
import { toArray, orderBy } from 'lodash';
let api = {};
/* NOTE most inbox routes are either in the user or members controller */
/**
* @api {get} /api/v3/inbox/messages Get inbox messages for a user
* @apiPrivate
* @apiName GetInboxMessages
* @apiGroup Inbox
* @apiDescription Get inbox messages for a user
*
* @apiSuccess {Array} data An array of inbox messages
*/
api.getInboxMessages = {
method: 'GET',
url: '/inbox/messages',
middlewares: [authWithHeaders()],
async handler (req, res) {
const messagesObj = res.locals.user.inbox.messages;
const messagesArray = orderBy(toArray(messagesObj), ['timestamp'], ['desc']);
res.respond(200, messagesArray);
},
};
module.exports = api;

View File

@@ -1,28 +1,49 @@
import { authWithHeaders } from '../../middlewares/auth';
import { toArray, orderBy } from 'lodash';
import apiError from '../../libs/apiError';
import * as inboxLib from '../../libs/inbox';
import {
NotFound,
} from '../../libs/errors';
let api = {};
const api = {};
/* NOTE most inbox routes are either in the user or members controller */
/**
* @api {get} /api/v3/inbox/messages Get inbox messages for a user
* @apiPrivate
* @apiName GetInboxMessages
* @apiGroup Inbox
* @apiDescription Get inbox messages for a user
*
* @apiSuccess {Array} data An array of inbox messages
*/
api.getInboxMessages = {
method: 'GET',
url: '/inbox/messages',
middlewares: [authWithHeaders()],
async handler (req, res) {
const messagesObj = res.locals.user.inbox.messages;
const messagesArray = orderBy(toArray(messagesObj), ['timestamp'], ['desc']);
/* NOTE the getInboxMessages route is implemented in v3 only */
res.respond(200, messagesArray);
/* NOTE this route has also an API v3 version */
/**
* @api {delete} /api/v4/inbox/messages/:messageId Delete a message
* @apiName deleteMessage
* @apiGroup User
*
* @apiParam (Path) {UUID} messageId The id of the message to delete
*
* @apiSuccess {Object} data Empty object
* @apiSuccessExample {json}
* {
* "success": true,
* "data": {}
* }
*/
api.deleteMessage = {
method: 'DELETE',
middlewares: [authWithHeaders()],
url: '/inbox/messages/:messageId',
async handler (req, res) {
req.checkParams('messageId', apiError('messageIdRequired')).notEmpty().isUUID();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const messageId = req.params.messageId;
const user = res.locals.user;
const deleted = await inboxLib.deleteMessage(user, messageId);
if (!deleted) throw new NotFound(res.t('messageGroupChatNotFound'));
res.respond(200);
},
};

View File

@@ -0,0 +1,11 @@
export async function deleteMessage (user, messageId) {
if (user.inbox.messages[messageId]) {
delete user.inbox.messages[messageId];
user.markModified(`inbox.messages.${messageId}`);
await user.save();
} else {
return false;
}
return true;
}

View File

@@ -52,7 +52,7 @@ module.exports.walkControllers = function walkControllers (router, filePath, ove
.readdirSync(filePath)
.forEach(fileName => {
if (!fs.statSync(filePath + fileName).isFile()) {
walkControllers(router, `${filePath}${fileName}/`);
walkControllers(router, `${filePath}${fileName}/`, overrides);
} else if (fileName.match(/\.js$/)) {
let controller = require(filePath + fileName); // eslint-disable-line global-require
module.exports.readController(router, controller, overrides);

View File

@@ -106,8 +106,8 @@ schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, o
// whether to save users after sending the message, defaults to true
let saveUsers = options.save === false ? false : true;
const newMessage = chatDefaults(options.receiverMsg, sender);
common.refPush(userToReceiveMessage.inbox.messages, newMessage);
const newMessageReceiver = chatDefaults(options.receiverMsg, sender);
common.refPush(userToReceiveMessage.inbox.messages, newMessageReceiver);
userToReceiveMessage.inbox.newMessages++;
userToReceiveMessage._v++;
userToReceiveMessage.markModified('inbox.messages');
@@ -134,7 +134,8 @@ schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, o
*/
common.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(senderMsg, userToReceiveMessage)));
const newMessage = defaults({sent: true}, chatDefaults(senderMsg, userToReceiveMessage));
common.refPush(sender.inbox.messages, newMessage);
sender.markModified('inbox.messages');
if (saveUsers) {