From b333d0c986b375fab8d75787c928c359bc4eb9c9 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 5 Jun 2023 19:21:47 +0530 Subject: [PATCH] feat: Attachments view (#7156) * feat: Attachments view with key shortcuts and dynamically updates when user delete or sent new attachments --------- Co-authored-by: Muhsin Keloth --- .../dashboard/api/inbox/conversation.js | 4 + .../api/specs/inbox/conversation.spec.js | 7 + .../scss/widgets/_conversation-view.scss | 19 +- .../widgets/conversation/Message.vue | 26 +-- .../widgets/conversation/MessagesView.vue | 5 + .../conversation/bubble/ImageAudioVideo.vue | 106 +++++++++ .../conversation/components/GalleryView.vue | 201 ++++++++++++++++++ .../store/modules/conversations/actions.js | 18 ++ .../store/modules/conversations/getters.js | 5 + .../store/modules/conversations/index.js | 46 ++++ .../specs/conversations/actions.spec.js | 43 +++- .../specs/conversations/getters.spec.js | 30 +++ .../specs/conversations/mutations.spec.js | 117 ++++++++++ .../dashboard/store/mutation-types.js | 4 + .../shared/helpers/KeyboardHelpers.js | 8 + 15 files changed, 607 insertions(+), 32 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/conversation/bubble/ImageAudioVideo.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index bee3cd23b..94cc81354 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -127,6 +127,10 @@ class ConversationApi extends ApiClient { user_ids: userIds, }); } + + getAllAttachments(conversationId) { + return axios.get(`${this.url}/${conversationId}/attachments`); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js index d276b8053..08343b69c 100644 --- a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -210,5 +210,12 @@ describe('#ConversationAPI', () => { { params: { page: payload.page } } ); }); + + it('#getAllAttachments', () => { + conversationAPI.getAllAttachments(1); + expect(context.axiosMock.get).toHaveBeenCalledWith( + '/api/v1/conversations/1/attachments' + ); + }); }); }); diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 6c37e5677..f95ee8a65 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -29,13 +29,13 @@ } .modal-image { - max-height: 90%; - max-width: 90%; + max-height: 80vh; + max-width: 80vw; } .modal-video { - max-height: 75vh; - max-width: 100%; + max-height: 80vh; + max-width: 80vw; } &::before { @@ -53,16 +53,6 @@ width: 100%; } } - - .video { - .modal-container { - width: auto; - - .modal--close { - z-index: var(--z-index-low); - } - } - } } .conversations-list-wrap { @@ -400,4 +390,3 @@ margin-bottom: 0; } } - diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index dd51ea838..4df910cde 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -53,22 +53,11 @@
- - - 0) { const { attachments = [{}] } = this.data; diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 23ebdc23e..5bac4a631 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -279,6 +279,7 @@ export default { if (newChat.id === oldChat.id) { return; } + this.fetchAllAttachmentsFromCurrentChat(); this.selectedTweetId = null; }, }, @@ -290,6 +291,7 @@ export default { mounted() { this.addScrollListener(); + this.fetchAllAttachmentsFromCurrentChat(); }, beforeDestroy() { @@ -298,6 +300,9 @@ export default { }, methods: { + fetchAllAttachmentsFromCurrentChat() { + this.$store.dispatch('fetchAllAttachments', this.currentChat.id); + }, removeBusListeners() { bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage); bus.$off(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet); diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/ImageAudioVideo.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/ImageAudioVideo.vue new file mode 100644 index 000000000..f703fefe2 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/ImageAudioVideo.vue @@ -0,0 +1,106 @@ + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue b/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue new file mode 100644 index 000000000..0f04c1d07 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue @@ -0,0 +1,201 @@ + + + diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index a5d4a25df..8db97fc88 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -86,6 +86,18 @@ const actions = { } }, + fetchAllAttachments: async ({ commit }, conversationId) => { + try { + const { data } = await ConversationApi.getAllAttachments(conversationId); + commit(types.SET_ALL_ATTACHMENTS, { + id: conversationId, + data: data.payload, + }); + } catch (error) { + // Handle error + } + }, + syncActiveConversationMessages: async ( { commit, state, dispatch }, { conversationId } @@ -247,6 +259,10 @@ const actions = { ...response.data, status: MESSAGE_STATUS.SENT, }); + commit(types.ADD_CONVERSATION_ATTACHMENTS, { + ...response.data, + status: MESSAGE_STATUS.SENT, + }); } catch (error) { const errorMessage = error.response ? error.response.data.error @@ -269,6 +285,7 @@ const actions = { conversationId: message.conversation_id, canReply: true, }); + commit(types.ADD_CONVERSATION_ATTACHMENTS, message); } }, @@ -283,6 +300,7 @@ const actions = { try { const { data } = await MessageApi.delete(conversationId, messageId); commit(types.ADD_MESSAGE, data); + commit(types.DELETE_CONVERSATION_ATTACHMENTS, data); } catch (error) { throw new Error(error); } diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index b5b38da0f..0b1ca73d3 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -32,6 +32,11 @@ const getters = { ); return selectedChat || {}; }, + getSelectedChatAttachments: (_state, _getters) => { + const selectedChat = _getters.getSelectedChat; + const { attachments } = selectedChat; + return attachments; + }, getLastEmailInSelectedChat: (stage, _getters) => { const selectedChat = _getters.getSelectedChat; const { messages = [] } = selectedChat; diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index 04db1fddf..2062b07da 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -3,6 +3,7 @@ import types from '../../mutation-types'; import getters, { getSelectedChatConversation } from './getters'; import actions from './actions'; import { findPendingMessageIndex } from './helpers'; +import { MESSAGE_STATUS } from 'shared/constants/messages'; import wootConstants from 'dashboard/constants/globals'; import { BUS_EVENTS } from '../../../../shared/constants/busEvents'; @@ -56,6 +57,13 @@ export const mutations = { chat.messages.unshift(...data); } }, + [types.SET_ALL_ATTACHMENTS](_state, { id, data }) { + if (data.length) { + const [chat] = _state.allConversations.filter(c => c.id === id); + Vue.set(chat, 'attachments', []); + chat.attachments.push(...data); + } + }, [types.SET_MISSING_MESSAGES](_state, { id, data }) { const [chat] = _state.allConversations.filter(c => c.id === id); if (!chat) return; @@ -115,6 +123,44 @@ export const mutations = { Vue.set(chat, 'muted', false); }, + [types.ADD_CONVERSATION_ATTACHMENTS]({ allConversations }, message) { + const { conversation_id: conversationId } = message; + const [chat] = getSelectedChatConversation({ + allConversations, + selectedChatId: conversationId, + }); + + if (!chat) return; + + const isMessageSent = + message.status === MESSAGE_STATUS.SENT && message.attachments; + if (isMessageSent) { + message.attachments.forEach(attachment => { + if (!chat.attachments.some(a => a.id === attachment.id)) { + chat.attachments.push(attachment); + } + }); + } + }, + + [types.DELETE_CONVERSATION_ATTACHMENTS]({ allConversations }, message) { + const { conversation_id: conversationId } = message; + const [chat] = getSelectedChatConversation({ + allConversations, + selectedChatId: conversationId, + }); + + if (!chat) return; + + const isMessageSent = message.status === MESSAGE_STATUS.SENT; + if (isMessageSent) { + const attachmentIndex = chat.attachments.findIndex( + a => a.message_id === message.id + ); + if (attachmentIndex !== -1) chat.attachments.splice(attachmentIndex, 1); + } + }, + [types.ADD_MESSAGE]({ allConversations, selectedChatId }, message) { const { conversation_id: conversationId } = message; const [chat] = getSelectedChatConversation({ diff --git a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js index b2d68c94f..5ff27c03e 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js @@ -204,6 +204,7 @@ describe('#actions', () => { ]); }); }); + describe('#addMessage', () => { it('sends correct mutations if message is incoming', () => { const message = { @@ -218,6 +219,7 @@ describe('#actions', () => { types.SET_CONVERSATION_CAN_REPLY, { conversationId: 1, canReply: true }, ], + [types.ADD_CONVERSATION_ATTACHMENTS, message], ]); }); it('sends correct mutations if message is not an incoming message', () => { @@ -436,10 +438,13 @@ describe('#actions', () => { describe('#deleteMessage', () => { it('sends correct actions if API is success', async () => { const [conversationId, messageId] = [1, 1]; - axios.delete.mockResolvedValue({ data: { id: 1, content: 'deleted' } }); + axios.delete.mockResolvedValue({ + data: { id: 1, content: 'deleted' }, + }); await actions.deleteMessage({ commit }, { conversationId, messageId }); expect(commit.mock.calls).toEqual([ [types.ADD_MESSAGE, { id: 1, content: 'deleted' }], + [types.DELETE_CONVERSATION_ATTACHMENTS, { id: 1, content: 'deleted' }], ]); }); it('sends no actions if API is error', async () => { @@ -554,4 +559,40 @@ describe('#addMentions', () => { ], ]); }); + + describe('#fetchAllAttachments', () => { + it('fetches all attachments', async () => { + axios.get.mockResolvedValue({ + data: { + payload: [ + { + id: 1, + message_id: 1, + file_type: 'image', + data_url: '', + thumb_url: '', + }, + ], + }, + }); + await actions.fetchAllAttachments({ commit }, 1); + expect(commit.mock.calls).toEqual([ + [ + types.SET_ALL_ATTACHMENTS, + { + id: 1, + data: [ + { + id: 1, + message_id: 1, + file_type: 'image', + data_url: '', + thumb_url: '', + }, + ], + }, + ], + ]); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index 2a012fd53..e874b05ec 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -305,4 +305,34 @@ describe('#getters', () => { }); }); }); + + describe('#getSelectedChatAttachments', () => { + it('Returns attachments in selected chat', () => { + const state = {}; + const getSelectedChat = { + attachments: [ + { + id: 1, + file_name: 'test1', + }, + { + id: 2, + file_name: 'test2', + }, + ], + }; + expect( + getters.getSelectedChatAttachments(state, { getSelectedChat }) + ).toEqual([ + { + id: 1, + file_name: 'test1', + }, + { + id: 2, + file_name: 'test2', + }, + ]); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js index 2c4bdb2cc..c7280c6f9 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js @@ -278,4 +278,121 @@ describe('#mutations', () => { expect(state.appliedFilters).toEqual([]); }); }); + + describe('#SET_ALL_ATTACHMENTS', () => { + it('set all attachments', () => { + const state = { + allConversations: [{ id: 1 }], + }; + const data = [{ id: 1, name: 'test' }]; + mutations[types.SET_ALL_ATTACHMENTS](state, { id: 1, data }); + expect(state.allConversations[0].attachments).toEqual(data); + }); + }); + + describe('#ADD_CONVERSATION_ATTACHMENTS', () => { + it('add conversation attachments', () => { + const state = { + allConversations: [{ id: 1, attachments: [] }], + }; + const message = { + conversation_id: 1, + status: 'sent', + attachments: [{ id: 1, name: 'test' }], + }; + + mutations[types.ADD_CONVERSATION_ATTACHMENTS](state, message); + expect(state.allConversations[0].attachments).toEqual( + message.attachments + ); + }); + + it('should not add duplicate attachments', () => { + const state = { + allConversations: [ + { + id: 1, + attachments: [{ id: 1, name: 'existing' }], + }, + ], + }; + const message = { + conversation_id: 1, + status: 'sent', + attachments: [ + { id: 1, name: 'existing' }, + { id: 2, name: 'new' }, + ], + }; + + mutations[types.ADD_CONVERSATION_ATTACHMENTS](state, message); + expect(state.allConversations[0].attachments).toHaveLength(2); + expect(state.allConversations[0].attachments).toContainEqual({ + id: 1, + name: 'existing', + }); + expect(state.allConversations[0].attachments).toContainEqual({ + id: 2, + name: 'new', + }); + }); + + it('should not add attachments if chat not found', () => { + const state = { + allConversations: [{ id: 1, attachments: [] }], + }; + const message = { + conversation_id: 2, + status: 'sent', + attachments: [{ id: 1, name: 'test' }], + }; + + mutations[types.ADD_CONVERSATION_ATTACHMENTS](state, message); + expect(state.allConversations[0].attachments).toHaveLength(0); + }); + }); + + describe('#DELETE_CONVERSATION_ATTACHMENTS', () => { + it('delete conversation attachments', () => { + const state = { + allConversations: [{ id: 1, attachments: [{ id: 1, message_id: 1 }] }], + }; + const message = { + conversation_id: 1, + status: 'sent', + id: 1, + }; + + mutations[types.DELETE_CONVERSATION_ATTACHMENTS](state, message); + expect(state.allConversations[0].attachments).toHaveLength(0); + }); + + it('should not delete attachments for non-matching message id', () => { + const state = { + allConversations: [{ id: 1, attachments: [{ id: 1, message_id: 1 }] }], + }; + const message = { + conversation_id: 1, + status: 'sent', + id: 2, + }; + + mutations[types.DELETE_CONVERSATION_ATTACHMENTS](state, message); + expect(state.allConversations[0].attachments).toHaveLength(1); + }); + + it('should not delete attachments if chat not found', () => { + const state = { + allConversations: [{ id: 1, attachments: [{ id: 1, message_id: 1 }] }], + }; + const message = { + conversation_id: 2, + status: 'sent', + id: 1, + }; + + mutations[types.DELETE_CONVERSATION_ATTACHMENTS](state, message); + expect(state.allConversations[0].attachments).toHaveLength(1); + }); + }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index ae15ff572..2f73080d3 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -49,6 +49,10 @@ export default { UPDATE_CONVERSATION_LAST_ACTIVITY: 'UPDATE_CONVERSATION_LAST_ACTIVITY', SET_MISSING_MESSAGES: 'SET_MISSING_MESSAGES', + SET_ALL_ATTACHMENTS: 'SET_ALL_ATTACHMENTS', + ADD_CONVERSATION_ATTACHMENTS: 'ADD_CONVERSATION_ATTACHMENTS', + DELETE_CONVERSATION_ATTACHMENTS: 'DELETE_CONVERSATION_ATTACHMENTS', + SET_CONVERSATION_CAN_REPLY: 'SET_CONVERSATION_CAN_REPLY', // Inboxes diff --git a/app/javascript/shared/helpers/KeyboardHelpers.js b/app/javascript/shared/helpers/KeyboardHelpers.js index 5ef9d9f6a..11e5af598 100644 --- a/app/javascript/shared/helpers/KeyboardHelpers.js +++ b/app/javascript/shared/helpers/KeyboardHelpers.js @@ -94,6 +94,14 @@ export const hasPressedArrowDownKey = e => { return e.keyCode === 40; }; +export const hasPressedArrowLeftKey = e => { + return e.keyCode === 37; +}; + +export const hasPressedArrowRightKey = e => { + return e.keyCode === 39; +}; + export const hasPressedCommandPlusKKey = e => { return e.metaKey && e.keyCode === 75; };