diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index ee5354027..b25ca4bbb 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -122,7 +122,6 @@ import ChatListHeader from './ChatListHeader.vue'; import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue'; import ChatTypeTabs from './widgets/ChatTypeTabs.vue'; import ConversationItem from './ConversationItem.vue'; -import timeMixin from '../mixins/time'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import conversationMixin from '../mixins/conversations'; import wootConstants from 'dashboard/constants/globals'; @@ -159,7 +158,6 @@ export default { VirtualList, }, mixins: [ - timeMixin, conversationMixin, keyboardEventListenerMixins, alertMixin, diff --git a/app/javascript/dashboard/components/ui/TimeAgo.vue b/app/javascript/dashboard/components/ui/TimeAgo.vue index 787c53e1e..14dcc82d5 100644 --- a/app/javascript/dashboard/components/ui/TimeAgo.vue +++ b/app/javascript/dashboard/components/ui/TimeAgo.vue @@ -5,7 +5,7 @@ delay: { show: 1500, hide: 0 }, hideOnClick: true, }" - class="text-xxs text-slate-500 dark:text-slate-500 leading-4 ml-auto hover:text-slate-900 dark:hover:text-slate-100" + class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100" > {{ `${createdAtTime} • ${lastActivityTime}` }} @@ -16,11 +16,14 @@ const MINUTE_IN_MILLI_SECONDS = 60000; const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60; const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24; -import timeMixin from 'dashboard/mixins/time'; +import { + dynamicTime, + dateFormat, + shortTimestamp, +} from 'shared/helpers/timeHelper'; export default { name: 'TimeAgo', - mixins: [timeMixin], props: { isAutoRefreshEnabled: { type: Boolean, @@ -37,17 +40,17 @@ export default { }, data() { return { - lastActivityAtTimeAgo: this.dynamicTime(this.lastActivityTimestamp), - createdAtTimeAgo: this.dynamicTime(this.createdAtTimestamp), + lastActivityAtTimeAgo: dynamicTime(this.lastActivityTimestamp), + createdAtTimeAgo: dynamicTime(this.createdAtTimestamp), timer: null, }; }, computed: { lastActivityTime() { - return this.shortTimestamp(this.lastActivityAtTimeAgo); + return shortTimestamp(this.lastActivityAtTimeAgo); }, createdAtTime() { - return this.shortTimestamp(this.createdAtTimeAgo); + return shortTimestamp(this.createdAtTimeAgo); }, createdAt() { const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000; @@ -56,9 +59,9 @@ export default { ? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${ this.createdAtTimeAgo }` - : `${this.$t( - 'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST' - )} ${this.dateFormat(this.createdAtTimestamp)}`; + : `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST')} ${dateFormat( + this.createdAtTimestamp + )}`; }, lastActivity() { const lastActivityTimeDiff = @@ -70,7 +73,7 @@ export default { }` : `${this.$t( 'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE' - )} ${this.dateFormat(this.lastActivityTimestamp)}`; + )} ${dateFormat(this.lastActivityTimestamp)}`; }, tooltipText() { return `${this.createdAt} @@ -79,10 +82,10 @@ export default { }, watch: { lastActivityTimestamp() { - this.lastActivityAtTimeAgo = this.dynamicTime(this.lastActivityTimestamp); + this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp); }, createdAtTimestamp() { - this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp); + this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp); }, }, mounted() { @@ -96,10 +99,8 @@ export default { methods: { createTimer() { this.timer = setTimeout(() => { - this.lastActivityAtTimeAgo = this.dynamicTime( - this.lastActivityTimestamp - ); - this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp); + this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp); + this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp); this.createTimer(); }, this.refreshTime()); }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 777c9fdcd..944ac369e 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -120,7 +120,6 @@ import { mapGetters } from 'vuex'; import Thumbnail from '../Thumbnail.vue'; import MessagePreview from './MessagePreview.vue'; import conversationMixin from '../../../mixins/conversations'; -import timeMixin from '../../../mixins/time'; import router from '../../../routes'; import { frontendURL, conversationUrl } from '../../../helper/URLHelper'; import InboxName from '../InboxName.vue'; @@ -144,7 +143,7 @@ export default { SLACardLabel, }, - mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin], + mixins: [inboxMixin, conversationMixin, alertMixin], props: { activeLabel: { type: String, diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index 416af5d06..b7b8ce2e5 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -77,10 +77,10 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import inboxMixin from 'shared/mixins/inboxMixin'; import { mapGetters } from 'vuex'; -import timeMixin from '../../../../mixins/time'; +import { messageTimestamp } from 'shared/helpers/timeHelper'; export default { - mixins: [inboxMixin, timeMixin], + mixins: [inboxMixin], props: { sender: { type: Object, @@ -159,7 +159,7 @@ export default { return MESSAGE_STATUS.SENT === this.messageStatus; }, readableTime() { - return this.messageTimestamp(this.createdAt, 'LLL d, h:mm a'); + return messageTimestamp(this.createdAt, 'LLL d, h:mm a'); }, screenName() { const { additional_attributes: additionalAttributes = {} } = diff --git a/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue b/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue index 08f77be9a..8684134ca 100644 --- a/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue @@ -12,7 +12,7 @@ @click="onClose" >
{{ $t('AUDIT_LOGS.LIST.404') }}
@@ -16,7 +16,7 @@| + | {{ generateLogText(auditLogItem) }} | -+ | {{ messageTimestamp( auditLogItem.created_at, @@ -65,7 +65,7 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue index 1e89f06ef..d78b97666 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatTable.vue @@ -26,7 +26,7 @@ import { VeTable, VePagination } from 'vue-easytable'; import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue'; import { CSAT_RATINGS } from 'shared/constants/messages'; import { mapGetters } from 'vuex'; -import timeMixin from 'dashboard/mixins/time'; +import { messageStamp, dynamicTime } from 'shared/helpers/timeHelper'; import rtlMixin from 'shared/mixins/rtlMixin'; export default { @@ -34,7 +34,7 @@ export default { VeTable, VePagination, }, - mixins: [timeMixin, rtlMixin], + mixins: [rtlMixin], props: { pageIndex: { type: Number, @@ -137,8 +137,8 @@ export default { rating: response.rating, feedbackText: response.feedback_message || '---', conversationId: response.conversation_id, - createdAgo: this.dynamicTime(response.created_at), - createdAt: this.messageStamp(response.created_at, 'LLL d yyyy, h:mm a'), + createdAgo: dynamicTime(response.created_at), + createdAt: messageStamp(response.created_at, 'LLL d yyyy, h:mm a'), })); }, }, diff --git a/app/javascript/shared/helpers/specs/timeHelper.spec.js b/app/javascript/shared/helpers/specs/timeHelper.spec.js new file mode 100644 index 000000000..13d04568f --- /dev/null +++ b/app/javascript/shared/helpers/specs/timeHelper.spec.js @@ -0,0 +1,92 @@ +import { + messageStamp, + messageTimestamp, + dynamicTime, + dateFormat, + shortTimestamp, +} from 'shared/helpers/timeHelper'; + +beforeEach(() => { + process.env.TZ = 'UTC'; + vi.useFakeTimers('modern'); + const mockDate = new Date(Date.UTC(2023, 4, 5)); + vi.setSystemTime(mockDate); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe('#messageStamp', () => { + it('returns correct value', () => { + expect(messageStamp(1612971343)).toEqual('3:35 PM'); + expect(messageStamp(1612971343, 'LLL d, h:mm a')).toEqual( + 'Feb 10, 3:35 PM' + ); + }); +}); + +describe('#messageTimestamp', () => { + it('should return the message date in the specified format if the message was sent in the current year', () => { + expect(messageTimestamp(1680777464)).toEqual('Apr 6, 2023'); + }); + it('should return the message date and time in a different format if the message was sent in a different year', () => { + expect(messageTimestamp(1612971343)).toEqual('Feb 10 2021, 3:35 PM'); + }); +}); + +describe('#dynamicTime', () => { + it('returns correct value', () => { + Date.now = vi.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf()); + expect(dynamicTime(1612971343)).toEqual('about 2 years ago'); + }); +}); + +describe('#dateFormat', () => { + it('returns correct value', () => { + expect(dateFormat(1612971343)).toEqual('Feb 10, 2021'); + expect(dateFormat(1612971343, 'LLL d, yyyy')).toEqual('Feb 10, 2021'); + }); +}); + +describe('#shortTimestamp', () => { + // Test cases when withAgo is false or not provided + it('returns correct value without ago', () => { + expect(shortTimestamp('less than a minute ago')).toEqual('now'); + expect(shortTimestamp('1 minute ago')).toEqual('1m'); + expect(shortTimestamp('12 minutes ago')).toEqual('12m'); + expect(shortTimestamp('a minute ago')).toEqual('1m'); + expect(shortTimestamp('an hour ago')).toEqual('1h'); + expect(shortTimestamp('1 hour ago')).toEqual('1h'); + expect(shortTimestamp('2 hours ago')).toEqual('2h'); + expect(shortTimestamp('1 day ago')).toEqual('1d'); + expect(shortTimestamp('a day ago')).toEqual('1d'); + expect(shortTimestamp('3 days ago')).toEqual('3d'); + expect(shortTimestamp('a month ago')).toEqual('1mo'); + expect(shortTimestamp('1 month ago')).toEqual('1mo'); + expect(shortTimestamp('2 months ago')).toEqual('2mo'); + expect(shortTimestamp('a year ago')).toEqual('1y'); + expect(shortTimestamp('1 year ago')).toEqual('1y'); + expect(shortTimestamp('4 years ago')).toEqual('4y'); + }); + + // Test cases when withAgo is true + it('returns correct value with ago', () => { + expect(shortTimestamp('less than a minute ago', true)).toEqual('now'); + expect(shortTimestamp('1 minute ago', true)).toEqual('1m ago'); + expect(shortTimestamp('12 minutes ago', true)).toEqual('12m ago'); + expect(shortTimestamp('a minute ago', true)).toEqual('1m ago'); + expect(shortTimestamp('an hour ago', true)).toEqual('1h ago'); + expect(shortTimestamp('1 hour ago', true)).toEqual('1h ago'); + expect(shortTimestamp('2 hours ago', true)).toEqual('2h ago'); + expect(shortTimestamp('1 day ago', true)).toEqual('1d ago'); + expect(shortTimestamp('a day ago', true)).toEqual('1d ago'); + expect(shortTimestamp('3 days ago', true)).toEqual('3d ago'); + expect(shortTimestamp('a month ago', true)).toEqual('1mo ago'); + expect(shortTimestamp('1 month ago', true)).toEqual('1mo ago'); + expect(shortTimestamp('2 months ago', true)).toEqual('2mo ago'); + expect(shortTimestamp('a year ago', true)).toEqual('1y ago'); + expect(shortTimestamp('1 year ago', true)).toEqual('1y ago'); + expect(shortTimestamp('4 years ago', true)).toEqual('4y ago'); + }); +}); diff --git a/app/javascript/shared/helpers/timeHelper.js b/app/javascript/shared/helpers/timeHelper.js new file mode 100644 index 000000000..6e041c7ec --- /dev/null +++ b/app/javascript/shared/helpers/timeHelper.js @@ -0,0 +1,93 @@ +import { + format, + isSameYear, + fromUnixTime, + formatDistanceToNow, +} from 'date-fns'; + +/** + * Formats a Unix timestamp into a human-readable time format. + * @param {number} time - Unix timestamp. + * @param {string} [dateFormat='h:mm a'] - Desired format of the time. + * @returns {string} Formatted time string. + */ +export const messageStamp = (time, dateFormat = 'h:mm a') => { + const unixTime = fromUnixTime(time); + return format(unixTime, dateFormat); +}; + +/** + * Provides a formatted timestamp, adjusting the format based on the current year. + * @param {number} time - Unix timestamp. + * @param {string} [dateFormat='MMM d, yyyy'] - Desired date format. + * @returns {string} Formatted date string. + */ +export const messageTimestamp = (time, dateFormat = 'MMM d, yyyy') => { + const messageTime = fromUnixTime(time); + const now = new Date(); + const messageDate = format(messageTime, dateFormat); + if (!isSameYear(messageTime, now)) { + return format(messageTime, 'LLL d y, h:mm a'); + } + return messageDate; +}; + +/** + * Converts a Unix timestamp to a relative time string (e.g., 3 hours ago). + * @param {number} time - Unix timestamp. + * @returns {string} Relative time string. + */ +export const dynamicTime = time => { + const unixTime = fromUnixTime(time); + return formatDistanceToNow(unixTime, { addSuffix: true }); +}; + +/** + * Formats a Unix timestamp into a specified date format. + * @param {number} time - Unix timestamp. + * @param {string} [dateFormat='MMM d, yyyy'] - Desired date format. + * @returns {string} Formatted date string. + */ +export const dateFormat = (time, df = 'MMM d, yyyy') => { + const unixTime = fromUnixTime(time); + return format(unixTime, df); +}; + +/** + * Converts a detailed time description into a shorter format, optionally appending 'ago'. + * @param {string} time - Detailed time description (e.g., 'a minute ago'). + * @param {boolean} [withAgo=false] - Whether to append 'ago' to the result. + * @returns {string} Shortened time description. + */ +export const shortTimestamp = (time, withAgo = false) => { + // This function takes a time string and converts it to a short time string + // with the following format: 1m, 1h, 1d, 1mo, 1y + // The function also takes an optional boolean parameter withAgo + // which will add the word "ago" to the end of the time string + const suffix = withAgo ? ' ago' : ''; + const timeMappings = { + 'less than a minute ago': 'now', + 'a minute ago': `1m${suffix}`, + 'an hour ago': `1h${suffix}`, + 'a day ago': `1d${suffix}`, + 'a month ago': `1mo${suffix}`, + 'a year ago': `1y${suffix}`, + }; + // Check if the time string is one of the specific cases + if (timeMappings[time]) { + return timeMappings[time]; + } + const convertToShortTime = time + .replace(/about|over|almost|/g, '') + .replace(' minute ago', `m${suffix}`) + .replace(' minutes ago', `m${suffix}`) + .replace(' hour ago', `h${suffix}`) + .replace(' hours ago', `h${suffix}`) + .replace(' day ago', `d${suffix}`) + .replace(' days ago', `d${suffix}`) + .replace(' month ago', `mo${suffix}`) + .replace(' months ago', `mo${suffix}`) + .replace(' year ago', `y${suffix}`) + .replace(' years ago', `y${suffix}`); + return convertToShortTime; +}; diff --git a/app/javascript/widget/components/AgentMessage.vue b/app/javascript/widget/components/AgentMessage.vue index c38726031..2bf94b094 100755 --- a/app/javascript/widget/components/AgentMessage.vue +++ b/app/javascript/widget/components/AgentMessage.vue @@ -30,7 +30,7 @@ /> |