feat: Add dynamic 'Last snoozed' option with reactive updates
- Add 'UNTIL_LAST_CUSTOM_TIME' snooze option that shows saved custom time - Implement smart expiration: times expire when passed OR after 7 days - Add user-friendly format: 'Sat, 23 Aug, 8.16pm' - Fix caching issue: actions now update dynamically without page refresh - Auto-save custom times to localStorage for quick reuse - Add reactive event system for real-time UI updates - Include option filtering to show/hide based on saved time availability Improves UX by allowing users to quickly reuse their last custom snooze time without having to re-enter it every time.
This commit is contained in:
@@ -31,6 +31,7 @@ export default {
|
||||
const unixTimestamp = getUnixTime(this.snoozeTime);
|
||||
// Save the custom time to localStorage for future use
|
||||
saveLastCustomSnoozeTime(unixTimestamp);
|
||||
// The saveLastCustomSnoozeTime function will emit the event automatically
|
||||
this.$emit('chooseTime', this.snoozeTime);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { hasLastCustomSnoozeTime } from 'dashboard/helper/customSnoozeStorage';
|
||||
|
||||
import {
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
@@ -28,16 +27,7 @@ const SNOOZE_CONVERSATION_BULK_ACTIONS = [
|
||||
title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
children: () => {
|
||||
const availableOptions = Object.values(SNOOZE_OPTIONS);
|
||||
// Only include UNTIL_LAST_CUSTOM_TIME if there's a saved custom time
|
||||
return availableOptions.filter(option => {
|
||||
if (option === SNOOZE_OPTIONS.UNTIL_LAST_CUSTOM_TIME) {
|
||||
return hasLastCustomSnoozeTime();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
children: Object.values(SNOOZE_OPTIONS),
|
||||
},
|
||||
...createSnoozeHandlers(
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
@@ -76,8 +66,9 @@ export function useBulkActionsHotKeys() {
|
||||
const prepareActions = actions => {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: t(action.title),
|
||||
section: t(action.section),
|
||||
title: typeof action.title === 'string' ? t(action.title) : action.title,
|
||||
section:
|
||||
typeof action.section === 'string' ? t(action.section) : action.section,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
@@ -31,22 +31,25 @@ import {
|
||||
|
||||
import {
|
||||
OPEN_CONVERSATION_ACTIONS,
|
||||
SNOOZE_CONVERSATION_ACTIONS,
|
||||
RESOLVED_CONVERSATION_ACTIONS,
|
||||
SEND_TRANSCRIPT_ACTION,
|
||||
UNMUTE_ACTION,
|
||||
MUTE_ACTION,
|
||||
createSnoozeHandlers,
|
||||
} from 'dashboard/helper/commandbar/actions';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/helper/commandbar/events';
|
||||
import { ICON_SNOOZE_CONVERSATION } from 'dashboard/helper/commandbar/icons';
|
||||
|
||||
const prepareActions = (actions, t) => {
|
||||
return actions.map(action => ({
|
||||
...action,
|
||||
title: t(action.title),
|
||||
section: t(action.section),
|
||||
title: typeof action.title === 'string' ? t(action.title) : action.title,
|
||||
section:
|
||||
typeof action.section === 'string' ? t(action.section) : action.section,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -144,6 +147,14 @@ export function useConversationHotKeys() {
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
// Create a reactive trigger for localStorage changes
|
||||
const localStorageTrigger = ref(0);
|
||||
|
||||
// Watch for localStorage changes (we'll trigger this manually when needed)
|
||||
const forceSnoozeActionsUpdate = () => {
|
||||
localStorageTrigger.value += 1;
|
||||
};
|
||||
|
||||
const {
|
||||
activeLabels,
|
||||
inactiveLabels,
|
||||
@@ -197,6 +208,28 @@ export function useConversationHotKeys() {
|
||||
});
|
||||
};
|
||||
|
||||
// Create reactive snooze conversation actions that update when localStorage changes
|
||||
const snoozeConversationActions = computed(() => {
|
||||
// This computed will re-run when localStorageTrigger changes
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
localStorageTrigger.value; // Access to make it reactive
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'snooze_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
children: Object.values(wootConstants.SNOOZE_OPTIONS),
|
||||
},
|
||||
...createSnoozeHandlers(
|
||||
CMD_SNOOZE_CONVERSATION,
|
||||
'snooze_conversation',
|
||||
'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION'
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
const statusActions = computed(() => {
|
||||
const isOpen = currentChat.value?.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
const isSnoozed =
|
||||
@@ -206,7 +239,10 @@ export function useConversationHotKeys() {
|
||||
|
||||
let actions = [];
|
||||
if (isOpen) {
|
||||
actions = [...OPEN_CONVERSATION_ACTIONS, ...SNOOZE_CONVERSATION_ACTIONS];
|
||||
actions = [
|
||||
...OPEN_CONVERSATION_ACTIONS,
|
||||
...snoozeConversationActions.value,
|
||||
];
|
||||
} else if (isResolved || isSnoozed) {
|
||||
actions = RESOLVED_CONVERSATION_ACTIONS;
|
||||
}
|
||||
@@ -394,7 +430,7 @@ export function useConversationHotKeys() {
|
||||
|
||||
const conversationHotKeys = computed(() => {
|
||||
if (shouldShowSnoozeOption.value) {
|
||||
return prepareActions(SNOOZE_CONVERSATION_ACTIONS, t);
|
||||
return prepareActions(snoozeConversationActions.value, t);
|
||||
}
|
||||
if (isConversationOrInboxRoute.value) {
|
||||
return getDefaultConversationHotKeys.value;
|
||||
@@ -404,5 +440,6 @@ export function useConversationHotKeys() {
|
||||
|
||||
return {
|
||||
conversationHotKeys,
|
||||
forceSnoozeActionsUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,21 +32,69 @@ export const OPEN_CONVERSATION_ACTIONS = [
|
||||
];
|
||||
|
||||
export const createSnoozeHandlers = (busEventName, parentId, section) => {
|
||||
// Only include UNTIL_LAST_CUSTOM_TIME in handlers if there's a saved custom time
|
||||
// Import the helper functions inline to avoid import issues
|
||||
const LAST_CUSTOM_SNOOZE_KEY = 'chatwoot_last_custom_snooze_time';
|
||||
const hasLastCustomSnoozeTime = () => {
|
||||
|
||||
const getLastCustomSnoozeTime = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem(LAST_CUSTOM_SNOOZE_KEY);
|
||||
if (!stored) return false;
|
||||
if (!stored) return null;
|
||||
|
||||
const data = JSON.parse(stored);
|
||||
const now = Date.now();
|
||||
const nowUnix = Math.floor(now / 1000);
|
||||
const savedAt = data.savedAt;
|
||||
const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;
|
||||
return now - data.savedAt <= sevenDaysInMs;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
// Check if the saved time has already passed OR if it was saved more than 7 days ago
|
||||
const timeHasPassed = data.timestamp <= nowUnix;
|
||||
const tooOld = now - savedAt > sevenDaysInMs;
|
||||
|
||||
if (timeHasPassed || tooOld) {
|
||||
localStorage.removeItem(LAST_CUSTOM_SNOOZE_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.timestamp;
|
||||
} catch (error) {
|
||||
localStorage.removeItem(LAST_CUSTOM_SNOOZE_KEY);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const formatLastCustomSnoozeTime = () => {
|
||||
const timestamp = getLastCustomSnoozeTime();
|
||||
if (!timestamp) return null;
|
||||
|
||||
try {
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
||||
// Format as "Sat, 23 Aug, 8.16pm"
|
||||
const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
const day = date.getDate();
|
||||
const month = date.toLocaleDateString('en-US', { month: 'short' });
|
||||
|
||||
// Format time as 8.16pm (with dots instead of colons)
|
||||
let hours = date.getHours();
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const ampm = hours >= 12 ? 'pm' : 'am';
|
||||
|
||||
// Convert to 12-hour format
|
||||
hours %= 12;
|
||||
if (hours === 0) hours = 12; // 12am/12pm instead of 0am/0pm
|
||||
|
||||
const timeString = `${hours}.${minutes}${ampm}`;
|
||||
|
||||
return `${dayOfWeek}, ${day} ${month}, ${timeString}`;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const hasLastCustomSnoozeTime = () => {
|
||||
return getLastCustomSnoozeTime() !== null;
|
||||
};
|
||||
|
||||
const availableOptions = Object.values(SNOOZE_OPTIONS);
|
||||
const snoozeOptions = availableOptions.filter(option => {
|
||||
if (option === SNOOZE_OPTIONS.UNTIL_LAST_CUSTOM_TIME) {
|
||||
@@ -55,14 +103,26 @@ export const createSnoozeHandlers = (busEventName, parentId, section) => {
|
||||
return true;
|
||||
});
|
||||
|
||||
return snoozeOptions.map(option => ({
|
||||
id: option,
|
||||
title: `COMMAND_BAR.COMMANDS.${option.toUpperCase()}`,
|
||||
parent: parentId,
|
||||
section: section,
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () => emitter.emit(busEventName, option),
|
||||
}));
|
||||
return snoozeOptions.map(option => {
|
||||
let title = `COMMAND_BAR.COMMANDS.${option.toUpperCase()}`;
|
||||
|
||||
// For the last custom time option, show the formatted time instead of using translation
|
||||
if (option === SNOOZE_OPTIONS.UNTIL_LAST_CUSTOM_TIME) {
|
||||
const formattedTime = formatLastCustomSnoozeTime();
|
||||
if (formattedTime) {
|
||||
title = `Last snoozed ${formattedTime}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: option,
|
||||
title: title,
|
||||
parent: parentId,
|
||||
section: section,
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () => emitter.emit(busEventName, option),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const SNOOZE_CONVERSATION_ACTIONS = [
|
||||
|
||||
@@ -11,6 +11,10 @@ export const saveLastCustomSnoozeTime = snoozedUntil => {
|
||||
savedAt: Date.now(),
|
||||
})
|
||||
);
|
||||
// Emit event to update command bar
|
||||
if (typeof window !== 'undefined' && window.emitter) {
|
||||
window.emitter.emit('CUSTOM_SNOOZE_TIME_CHANGED');
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to save custom snooze time:', error);
|
||||
@@ -23,15 +27,21 @@ export const getLastCustomSnoozeTime = () => {
|
||||
if (!stored) return null;
|
||||
|
||||
const data = JSON.parse(stored);
|
||||
|
||||
// Check if the saved time is still in the future (within 7 days of when it was saved)
|
||||
const now = Date.now();
|
||||
const nowUnix = Math.floor(now / 1000);
|
||||
const savedAt = data.savedAt;
|
||||
const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
// If saved more than 7 days ago, consider it stale
|
||||
if (now - savedAt > sevenDaysInMs) {
|
||||
// Check if the saved time has already passed OR if it was saved more than 7 days ago
|
||||
const timeHasPassed = data.timestamp <= nowUnix;
|
||||
const tooOld = now - savedAt > sevenDaysInMs;
|
||||
|
||||
if (timeHasPassed || tooOld) {
|
||||
localStorage.removeItem(LAST_CUSTOM_SNOOZE_KEY);
|
||||
// Emit event when data is cleared due to expiration
|
||||
if (typeof window !== 'undefined' && window.emitter) {
|
||||
window.emitter.emit('CUSTOM_SNOOZE_TIME_CHANGED');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -40,6 +50,10 @@ export const getLastCustomSnoozeTime = () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to get custom snooze time:', error);
|
||||
localStorage.removeItem(LAST_CUSTOM_SNOOZE_KEY);
|
||||
// Emit event when data is cleared due to error
|
||||
if (typeof window !== 'undefined' && window.emitter) {
|
||||
window.emitter.emit('CUSTOM_SNOOZE_TIME_CHANGED');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -54,7 +68,24 @@ export const formatLastCustomSnoozeTime = () => {
|
||||
|
||||
try {
|
||||
const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds
|
||||
return date.toLocaleString();
|
||||
|
||||
// Format as "Sat, 23 Aug, 8.16pm"
|
||||
const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
const day = date.getDate();
|
||||
const month = date.toLocaleDateString('en-US', { month: 'short' });
|
||||
|
||||
// Format time as 8.16pm (with dots instead of colons)
|
||||
let hours = date.getHours();
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const ampm = hours >= 12 ? 'pm' : 'am';
|
||||
|
||||
// Convert to 12-hour format
|
||||
hours %= 12;
|
||||
if (hours === 0) hours = 12; // 12am/12pm instead of 0am/0pm
|
||||
|
||||
const timeString = `${hours}.${minutes}${ampm}`;
|
||||
|
||||
return `${dayOfWeek}, ${day} ${month}, ${timeString}`;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
"UNTIL_TOMORROW": "Until tomorrow",
|
||||
"UNTIL_NEXT_MONTH": "Until next month",
|
||||
"AN_HOUR_FROM_NOW": "Until an hour from now",
|
||||
"UNTIL_LAST_CUSTOM_TIME": "Use last custom time",
|
||||
"UNTIL_LAST_CUSTOM_TIME": "Last snoozed {time}",
|
||||
"UNTIL_CUSTOM_TIME": "Custom...",
|
||||
"CHANGE_APPEARANCE": "Change Appearance",
|
||||
"LIGHT_MODE": "Light",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import '@chatwoot/ninja-keys';
|
||||
import { ref, computed, watchEffect, onMounted } from 'vue';
|
||||
import { ref, computed, watchEffect, onMounted, onUnmounted } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -11,6 +11,7 @@ import { useBulkActionsHotKeys } from 'dashboard/composables/commands/useBulkAct
|
||||
import { useConversationHotKeys } from 'dashboard/composables/commands/useConversationHotKeys';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { GENERAL_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
@@ -26,7 +27,8 @@ const { goToAppearanceHotKeys } = useAppearanceHotKeys();
|
||||
const { inboxHotKeys } = useInboxHotKeys();
|
||||
const { goToCommandHotKeys } = useGoToCommandHotKeys();
|
||||
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
|
||||
const { conversationHotKeys } = useConversationHotKeys();
|
||||
const { conversationHotKeys, forceSnoozeActionsUpdate } =
|
||||
useConversationHotKeys();
|
||||
|
||||
const placeholder = computed(() => t('COMMAND_BAR.SEARCH_PLACEHOLDER'));
|
||||
|
||||
@@ -78,7 +80,37 @@ watchEffect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(setCommandBarData);
|
||||
onMounted(() => {
|
||||
setCommandBarData();
|
||||
|
||||
// Make emitter globally available for storage helpers
|
||||
window.emitter = emitter;
|
||||
|
||||
// Listen for custom snooze time changes to update actions
|
||||
emitter.on('CUSTOM_SNOOZE_TIME_CHANGED', () => {
|
||||
if (forceSnoozeActionsUpdate) {
|
||||
forceSnoozeActionsUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
// Backward compatibility
|
||||
emitter.on('CUSTOM_SNOOZE_TIME_SAVED', () => {
|
||||
if (forceSnoozeActionsUpdate) {
|
||||
forceSnoozeActionsUpdate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up event listeners
|
||||
emitter.off('CUSTOM_SNOOZE_TIME_CHANGED');
|
||||
emitter.off('CUSTOM_SNOOZE_TIME_SAVED');
|
||||
|
||||
// Clean up global reference
|
||||
if (window.emitter === emitter) {
|
||||
delete window.emitter;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/attribute-hyphenation -->
|
||||
|
||||
Reference in New Issue
Block a user