mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	[Enhancement] Group widget messages by date (#363)
* [Enhancement] Group widget messages by date * Update DateSeparator snapshot
This commit is contained in:
		
							
								
								
									
										48
									
								
								app/javascript/shared/components/DateSeparator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/javascript/shared/components/DateSeparator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="date--separator"> | ||||||
|  |     {{ date }} | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     date: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import '~widget/assets/scss/variables'; | ||||||
|  |  | ||||||
|  | .date--separator { | ||||||
|  |   font-size: $font-size-default; | ||||||
|  |   color: $color-body; | ||||||
|  |   height: 50px; | ||||||
|  |   line-height: 50px; | ||||||
|  |   position: relative; | ||||||
|  |   text-align: center; | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .date--separator::before, | ||||||
|  | .date--separator::after { | ||||||
|  |   background-color: $color-border; | ||||||
|  |   content: ''; | ||||||
|  |   height: 1px; | ||||||
|  |   position: absolute; | ||||||
|  |   top: 24px; | ||||||
|  |   width: calc((100% - 120px) / 2); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .date--separator::before { | ||||||
|  |   left: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .date--separator::after { | ||||||
|  |   right: 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										14
									
								
								app/javascript/shared/components/specs/DateSeparator.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/javascript/shared/components/specs/DateSeparator.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import { mount } from '@vue/test-utils'; | ||||||
|  | import DateSeparator from '../DateSeparator'; | ||||||
|  |  | ||||||
|  | describe('Spinner', () => { | ||||||
|  |   test('matches snapshot', () => { | ||||||
|  |     const wrapper = mount(DateSeparator, { | ||||||
|  |       propsData: { | ||||||
|  |         date: 'Nov 18, 2019', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     expect(wrapper.isVueInstance()).toBeTruthy(); | ||||||
|  |     expect(wrapper.element).toMatchSnapshot(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||||
|  |  | ||||||
|  | exports[`Spinner matches snapshot 1`] = ` | ||||||
|  | <div | ||||||
|  |   class="date--separator" | ||||||
|  | > | ||||||
|  |    | ||||||
|  |   Nov 18, 2019 | ||||||
|  |  | ||||||
|  | </div> | ||||||
|  | `; | ||||||
							
								
								
									
										13
									
								
								app/javascript/shared/helpers/DateHelper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/javascript/shared/helpers/DateHelper.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import moment from 'moment'; | ||||||
|  |  | ||||||
|  | class DateHelper { | ||||||
|  |   constructor(date) { | ||||||
|  |     this.date = moment(date * 1000); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   format(dateFormat = 'MMM DD, YYYY') { | ||||||
|  |     return this.date.format(dateFormat); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default DateHelper; | ||||||
							
								
								
									
										13
									
								
								app/javascript/shared/helpers/specs/DateHelper.sepc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/javascript/shared/helpers/specs/DateHelper.sepc.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import DateSeparator from '../DateSeparator'; | ||||||
|  |  | ||||||
|  | describe('#DateSeparator', () => { | ||||||
|  |   it('should format correctly without dateFormat', () => { | ||||||
|  |     expect(new DateSeparator(1576340626).format()).toEqual('Dec 14, 2019'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should format correctly without dateFormat', () => { | ||||||
|  |     expect(new DateSeparator(1576340626).format('DD-MM-YYYY')).toEqual( | ||||||
|  |       '14-12-2019' | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -4,12 +4,15 @@ | |||||||
|       <div v-if="isFetchingList" class="message--loader"> |       <div v-if="isFetchingList" class="message--loader"> | ||||||
|         <spinner></spinner> |         <spinner></spinner> | ||||||
|       </div> |       </div> | ||||||
|  |       <div v-for="date in conversationDates" :key="date"> | ||||||
|  |         <date-separator :date="date"></date-separator> | ||||||
|         <ChatMessage |         <ChatMessage | ||||||
|         v-for="message in messages" |           v-for="message in groupedMessages[date]" | ||||||
|           :key="message.id" |           :key="message.id" | ||||||
|           :message="message" |           :message="message" | ||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|  |     </div> | ||||||
|     <branding></branding> |     <branding></branding> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @@ -17,6 +20,7 @@ | |||||||
| <script> | <script> | ||||||
| import Branding from 'widget/components/Branding.vue'; | import Branding from 'widget/components/Branding.vue'; | ||||||
| import ChatMessage from 'widget/components/ChatMessage.vue'; | import ChatMessage from 'widget/components/ChatMessage.vue'; | ||||||
|  | import DateSeparator from 'shared/components/DateSeparator.vue'; | ||||||
| import Spinner from 'shared/components/Spinner.vue'; | import Spinner from 'shared/components/Spinner.vue'; | ||||||
| import { mapActions, mapGetters } from 'vuex'; | import { mapActions, mapGetters } from 'vuex'; | ||||||
|  |  | ||||||
| @@ -25,10 +29,11 @@ export default { | |||||||
|   components: { |   components: { | ||||||
|     Branding, |     Branding, | ||||||
|     ChatMessage, |     ChatMessage, | ||||||
|  |     DateSeparator, | ||||||
|     Spinner, |     Spinner, | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|     messages: Object, |     groupedMessages: Object, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
| @@ -43,6 +48,9 @@ export default { | |||||||
|       isFetchingList: 'conversation/getIsFetchingList', |       isFetchingList: 'conversation/getIsFetchingList', | ||||||
|       conversationSize: 'conversation/getConversationSize', |       conversationSize: 'conversation/getConversationSize', | ||||||
|     }), |     }), | ||||||
|  |     conversationDates() { | ||||||
|  |       return Object.keys(this.groupedMessages); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|     allMessagesLoaded() { |     allMessagesLoaded() { | ||||||
|   | |||||||
| @@ -3,6 +3,9 @@ import Vue from 'vue'; | |||||||
| import { sendMessageAPI, getConversationAPI } from 'widget/api/conversation'; | import { sendMessageAPI, getConversationAPI } from 'widget/api/conversation'; | ||||||
| import { MESSAGE_TYPE } from 'widget/helpers/constants'; | import { MESSAGE_TYPE } from 'widget/helpers/constants'; | ||||||
| import getUuid from '../../helpers/uuid'; | import getUuid from '../../helpers/uuid'; | ||||||
|  | import DateHelper from '../../../shared/helpers/DateHelper'; | ||||||
|  |  | ||||||
|  | const groupBy = require('lodash.groupby'); | ||||||
|  |  | ||||||
| export const createTemporaryMessage = content => { | export const createTemporaryMessage = content => { | ||||||
|   const timestamp = new Date().getTime(); |   const timestamp = new Date().getTime(); | ||||||
| @@ -31,10 +34,9 @@ const state = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export const getters = { | export const getters = { | ||||||
|  |   getAllMessagesLoaded: _state => _state.uiFlags.allMessagesLoaded, | ||||||
|   getConversation: _state => _state.conversations, |   getConversation: _state => _state.conversations, | ||||||
|   getConversationSize: _state => Object.keys(_state.conversations).length, |   getConversationSize: _state => Object.keys(_state.conversations).length, | ||||||
|   getAllMessagesLoaded: _state => _state.uiFlags.allMessagesLoaded, |  | ||||||
|   getIsFetchingList: _state => _state.uiFlags.isFetchingList, |  | ||||||
|   getEarliestMessage: _state => { |   getEarliestMessage: _state => { | ||||||
|     const conversation = Object.values(_state.conversations); |     const conversation = Object.values(_state.conversations); | ||||||
|     if (conversation.length) { |     if (conversation.length) { | ||||||
| @@ -42,6 +44,12 @@ export const getters = { | |||||||
|     } |     } | ||||||
|     return {}; |     return {}; | ||||||
|   }, |   }, | ||||||
|  |   getGroupedConversation: _state => { | ||||||
|  |     return groupBy(Object.values(_state.conversations), message => | ||||||
|  |       new DateHelper(message.created_at).format() | ||||||
|  |     ); | ||||||
|  |   }, | ||||||
|  |   getIsFetchingList: _state => _state.uiFlags.isFetchingList, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const actions = { | export const actions = { | ||||||
|   | |||||||
| @@ -53,4 +53,57 @@ describe('#getters', () => { | |||||||
|     expect(getters.getAllMessagesLoaded(state)).toEqual(false); |     expect(getters.getAllMessagesLoaded(state)).toEqual(false); | ||||||
|     expect(getters.getIsFetchingList(state)).toEqual(false); |     expect(getters.getIsFetchingList(state)).toEqual(false); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   it('uiFlags', () => { | ||||||
|  |     const state = { | ||||||
|  |       conversations: { | ||||||
|  |         1: { | ||||||
|  |           id: 1, | ||||||
|  |           content: 'Thanks for the help', | ||||||
|  |           created_at: 1574075964, | ||||||
|  |         }, | ||||||
|  |         2: { | ||||||
|  |           id: 2, | ||||||
|  |           content: 'Yes, It makes sense', | ||||||
|  |           created_at: 1574092218, | ||||||
|  |         }, | ||||||
|  |         3: { | ||||||
|  |           id: 3, | ||||||
|  |           content: 'Hey', | ||||||
|  |           created_at: 1576340623, | ||||||
|  |         }, | ||||||
|  |         4: { | ||||||
|  |           id: 4, | ||||||
|  |           content: 'How may I help you', | ||||||
|  |           created_at: 1576340626, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |     expect(getters.getGroupedConversation(state)).toEqual({ | ||||||
|  |       'Nov 18, 2019': [ | ||||||
|  |         { | ||||||
|  |           id: 1, | ||||||
|  |           content: 'Thanks for the help', | ||||||
|  |           created_at: 1574075964, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           id: 2, | ||||||
|  |           content: 'Yes, It makes sense', | ||||||
|  |           created_at: 1574092218, | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |       'Dec 14, 2019': [ | ||||||
|  |         { | ||||||
|  |           id: 3, | ||||||
|  |           content: 'Hey', | ||||||
|  |           created_at: 1576340623, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           id: 4, | ||||||
|  |           content: 'How may I help you', | ||||||
|  |           created_at: 1576340626, | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|       <ChatHeaderExpanded v-if="isHeaderExpanded" /> |       <ChatHeaderExpanded v-if="isHeaderExpanded" /> | ||||||
|       <ChatHeader v-else :title="getHeaderName" /> |       <ChatHeader v-else :title="getHeaderName" /> | ||||||
|     </div> |     </div> | ||||||
|     <ConversationWrap :messages="getConversation" /> |     <ConversationWrap :grouped-messages="groupedMessages" /> | ||||||
|     <div class="footer-wrap"> |     <div class="footer-wrap"> | ||||||
|       <ChatFooter :on-send-message="handleSendMessage" /> |       <ChatFooter :on-send-message="handleSendMessage" /> | ||||||
|     </div> |     </div> | ||||||
| @@ -36,9 +36,12 @@ export default { | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapGetters('conversation', ['getConversation', 'getConversationSize']), |     ...mapGetters({ | ||||||
|  |       groupedMessages: 'conversation/getGroupedConversation', | ||||||
|  |       conversationSize: 'conversation/getConversationSize', | ||||||
|  |     }), | ||||||
|     isHeaderExpanded() { |     isHeaderExpanded() { | ||||||
|       return this.getConversationSize === 0; |       return this.conversationSize === 0; | ||||||
|     }, |     }, | ||||||
|     getHeaderName() { |     getHeaderName() { | ||||||
|       return window.chatwootWebChannel.website_name; |       return window.chatwootWebChannel.website_name; | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ | |||||||
|     "highlight.js": "^9.15.10", |     "highlight.js": "^9.15.10", | ||||||
|     "ionicons": "~2.0.1", |     "ionicons": "~2.0.1", | ||||||
|     "js-cookie": "^2.2.1", |     "js-cookie": "^2.2.1", | ||||||
|  |     "lodash.groupby": "^4.6.0", | ||||||
|     "md5": "~2.2.1", |     "md5": "~2.2.1", | ||||||
|     "moment": "~2.19.3", |     "moment": "~2.19.3", | ||||||
|     "query-string": "5", |     "query-string": "5", | ||||||
| @@ -91,7 +92,10 @@ | |||||||
|   }, |   }, | ||||||
|   "jest": { |   "jest": { | ||||||
|     "collectCoverage": true, |     "collectCoverage": true, | ||||||
|      "coverageReporters": ["lcov", "text"] |     "coverageReporters": [ | ||||||
|  |       "lcov", | ||||||
|  |       "text" | ||||||
|  |     ] | ||||||
|   }, |   }, | ||||||
|   "lint-staged": { |   "lint-staged": { | ||||||
|     "*.{js,vue}": [ |     "*.{js,vue}": [ | ||||||
|   | |||||||
| @@ -6220,6 +6220,11 @@ lodash.get@^4.0: | |||||||
|   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" |   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" | ||||||
|   integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= |   integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= | ||||||
|  |  | ||||||
|  | lodash.groupby@^4.6.0: | ||||||
|  |   version "4.6.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1" | ||||||
|  |   integrity sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E= | ||||||
|  |  | ||||||
| lodash.has@^4.0: | lodash.has@^4.0: | ||||||
|   version "4.5.2" |   version "4.5.2" | ||||||
|   resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" |   resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S