feat: update notification settings (#10529)
https://github.com/user-attachments/assets/52ecf3f8-0329-4268-906e-d6102338f4af --------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ defineProps({
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
const onChange = (id, value) => {
|
||||
emit('change', id, value);
|
||||
@@ -23,18 +24,22 @@ const onChange = (id, value) => {
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3 mt-2">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="flex flex-row items-start gap-2"
|
||||
>
|
||||
<CheckBox
|
||||
:id="`checkbox-condition-${item.value}`"
|
||||
:is-checked="item.model"
|
||||
:value="item.value"
|
||||
@update="onChange"
|
||||
/>
|
||||
<label class="text-sm font-normal text-ash-900">
|
||||
<label
|
||||
class="text-sm font-normal text-ash-900"
|
||||
:for="`checkbox-condition-${item.value}`"
|
||||
>
|
||||
{{ item.label }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ALERT_EVENTS } from './constants';
|
||||
import CheckBox from 'v3/components/Form/CheckBox.vue';
|
||||
import { ALERT_EVENTS, EVENT_TYPES } from './constants';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
@@ -9,50 +10,83 @@ const props = defineProps({
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: 'all',
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const alertEvents = ALERT_EVENTS;
|
||||
const alertEventValues = Object.values(EVENT_TYPES);
|
||||
|
||||
const selectedValue = computed({
|
||||
get: () => props.value,
|
||||
set: value => {
|
||||
emit('update', value);
|
||||
get: () => {
|
||||
// maintain backward compatibility
|
||||
if (props.value === 'none') return [];
|
||||
if (props.value === 'mine') return [EVENT_TYPES.ASSIGNED];
|
||||
if (props.value === 'all') return [...alertEventValues];
|
||||
|
||||
const validValues = props.value
|
||||
.split('+')
|
||||
.filter(value => alertEventValues.includes(value));
|
||||
|
||||
return [...new Set(validValues)];
|
||||
},
|
||||
set: value => {
|
||||
const sortedValues = value.filter(Boolean).sort();
|
||||
const uniqueValues = [...new Set(sortedValues)];
|
||||
|
||||
if (uniqueValues.length === 0) {
|
||||
emit('update', 'none');
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update', uniqueValues.join('+'));
|
||||
},
|
||||
});
|
||||
|
||||
const setValue = (isChecked, value) => {
|
||||
let updatedValue = selectedValue.value;
|
||||
if (isChecked) {
|
||||
updatedValue.push(value);
|
||||
} else {
|
||||
updatedValue = updatedValue.filter(item => item !== value);
|
||||
}
|
||||
|
||||
selectedValue.value = updatedValue;
|
||||
};
|
||||
|
||||
const alertDescription = computed(() => {
|
||||
const base =
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_COMBINATIONS.';
|
||||
|
||||
if (props.value === '' || props.value === 'none') {
|
||||
return base + 'NONE';
|
||||
}
|
||||
|
||||
return base + selectedValue.value.join('+').toUpperCase();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
class="flex justify-between pb-1 text-sm font-medium leading-6 text-ash-900"
|
||||
>
|
||||
<label class="pb-1 text-sm font-medium leading-6 text-ash-900">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div
|
||||
class="flex flex-row justify-between h-10 max-w-xl p-2 border border-solid rounded-xl border-ash-200"
|
||||
>
|
||||
<div class="grid gap-3 mt-2">
|
||||
<div
|
||||
v-for="option in alertEvents"
|
||||
:key="option.value"
|
||||
class="flex flex-row items-center justify-center gap-2 px-4 border-r border-ash-200 grow last:border-r-0"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
:id="`radio-${option.value}`"
|
||||
v-model="selectedValue"
|
||||
class="shadow-sm cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-4 h-4 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[14px] checked:before:h-[14px] checked:border checked:border-primary-600"
|
||||
type="radio"
|
||||
:value="option.value"
|
||||
<CheckBox
|
||||
:id="`checkbox-${option.value}`"
|
||||
:is-checked="selectedValue.includes(option.value)"
|
||||
@update="(_val, isChecked) => setValue(isChecked, option.value)"
|
||||
/>
|
||||
<label
|
||||
:for="`radio-${option.value}`"
|
||||
class="text-sm font-medium"
|
||||
:class="
|
||||
selectedValue === option.value ? 'text-ash-900' : 'text-ash-800'
|
||||
"
|
||||
:for="`checkbox-${option.value}`"
|
||||
class="text-sm text-ash-900 font-normal"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
@@ -61,6 +95,9 @@ const selectedValue = computed({
|
||||
}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="text-n-slate-11 text-sm font-medium mt-2">
|
||||
{{ $t(alertDescription) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import * as Sentry from '@sentry/vue';
|
||||
import FormSelect from 'v3/components/Form/Select.vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => ['ding', 'bell'].includes(value),
|
||||
validator: value =>
|
||||
['ding', 'bell', 'chime', 'magic', 'ping'].includes(value),
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
@@ -25,6 +28,18 @@ const alertTones = computed(() => [
|
||||
value: 'bell',
|
||||
label: 'Bell',
|
||||
},
|
||||
{
|
||||
value: 'chime',
|
||||
label: 'Chime',
|
||||
},
|
||||
{
|
||||
value: 'magic',
|
||||
label: 'Magic',
|
||||
},
|
||||
{
|
||||
value: 'ping',
|
||||
label: 'Ping',
|
||||
},
|
||||
]);
|
||||
|
||||
const selectedValue = computed({
|
||||
@@ -33,25 +48,48 @@ const selectedValue = computed({
|
||||
emit('change', value);
|
||||
},
|
||||
});
|
||||
|
||||
const audio = new Audio();
|
||||
|
||||
const playAudio = async () => {
|
||||
try {
|
||||
// Has great support https://caniuse.com/mdn-api_htmlaudioelement
|
||||
audio.src = `/audio/dashboard/${selectedValue.value}.mp3`;
|
||||
await audio.play();
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormSelect
|
||||
v-model="selectedValue"
|
||||
name="alertTone"
|
||||
spacing="compact"
|
||||
:value="selectedValue"
|
||||
:options="alertTones"
|
||||
:label="label"
|
||||
class="max-w-xl"
|
||||
>
|
||||
<option
|
||||
v-for="tone in alertTones"
|
||||
:key="tone.label"
|
||||
:value="tone.value"
|
||||
:selected="tone.value === selectedValue"
|
||||
<div class="flex items-center gap-2">
|
||||
<FormSelect
|
||||
v-model="selectedValue"
|
||||
name="alertTone"
|
||||
spacing="compact"
|
||||
class="flex-grow"
|
||||
:value="selectedValue"
|
||||
:options="alertTones"
|
||||
:label="label"
|
||||
>
|
||||
{{ tone.label }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
<option
|
||||
v-for="tone in alertTones"
|
||||
:key="tone.label"
|
||||
:value="tone.value"
|
||||
:selected="tone.value === selectedValue"
|
||||
>
|
||||
{{ tone.label }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
<button
|
||||
v-tooltip.top="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.PLAY')
|
||||
"
|
||||
class="border-0 shadow-sm outline-none flex justify-center items-center size-10 appearance-none rounded-xl ring-ash-200 ring-1 ring-inset focus:ring-2 focus:ring-inset focus:ring-primary-500 flex-shrink-0 mt-[28px]"
|
||||
@click="playAudio"
|
||||
>
|
||||
<Icon icon="i-lucide-volume-2" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,98 +1,91 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import AudioAlertTone from './AudioAlertTone.vue';
|
||||
import AudioAlertEvent from './AudioAlertEvent.vue';
|
||||
import AudioAlertCondition from './AudioAlertCondition.vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
const store = useStore();
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import { initializeAudioAlerts } from 'dashboard/helper/scriptHelpers';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AudioAlertEvent,
|
||||
AudioAlertTone,
|
||||
AudioAlertCondition,
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const getters = useStoreGetters();
|
||||
const currentUser = computed(() => getters.getCurrentUser.value);
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
audioAlert: '',
|
||||
playAudioWhenTabIsInactive: false,
|
||||
alertIfUnreadConversationExist: false,
|
||||
alertTone: 'ding',
|
||||
audioAlertConditions: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
uiSettings(value) {
|
||||
this.notificationUISettings(value);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.notificationUISettings(this.uiSettings);
|
||||
this.$store.dispatch('userNotificationSettings/get');
|
||||
},
|
||||
methods: {
|
||||
notificationUISettings(uiSettings) {
|
||||
const {
|
||||
enable_audio_alerts: audioAlert = '',
|
||||
always_play_audio_alert: alwaysPlayAudioAlert,
|
||||
alert_if_unread_assigned_conversation_exist:
|
||||
alertIfUnreadConversationExist,
|
||||
notification_tone: alertTone,
|
||||
} = uiSettings;
|
||||
this.audioAlert = audioAlert;
|
||||
this.playAudioWhenTabIsInactive = !alwaysPlayAudioAlert;
|
||||
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
|
||||
this.audioAlertConditions = [
|
||||
{
|
||||
id: 'audio1',
|
||||
label: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_ONE'
|
||||
),
|
||||
model: this.playAudioWhenTabIsInactive,
|
||||
value: 'tab_is_inactive',
|
||||
},
|
||||
{
|
||||
id: 'audio2',
|
||||
label: this.$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_TWO'
|
||||
),
|
||||
model: this.alertIfUnreadConversationExist,
|
||||
value: 'conversations_are_read',
|
||||
},
|
||||
];
|
||||
this.alertTone = alertTone || 'ding';
|
||||
},
|
||||
handAudioAlertChange(value) {
|
||||
this.audioAlert = value;
|
||||
this.updateUISettings({
|
||||
enable_audio_alerts: this.audioAlert,
|
||||
});
|
||||
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
handleAudioAlertConditions(id, value) {
|
||||
if (id === 'tab_is_inactive') {
|
||||
this.updateUISettings({
|
||||
always_play_audio_alert: !value,
|
||||
});
|
||||
} else if (id === 'conversations_are_read') {
|
||||
this.updateUISettings({
|
||||
alert_if_unread_assigned_conversation_exist: value,
|
||||
});
|
||||
}
|
||||
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
},
|
||||
handleAudioToneChange(value) {
|
||||
this.updateUISettings({ notification_tone: value });
|
||||
useAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const { t } = useI18n();
|
||||
const audioAlert = ref('');
|
||||
const playAudioWhenTabIsInactive = ref(false);
|
||||
const alertIfUnreadConversationExist = ref(false);
|
||||
const alertTone = ref('ding');
|
||||
const audioAlertConditions = ref([]);
|
||||
const i18nKeyPrefix = 'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION';
|
||||
|
||||
const initializeNotificationUISettings = newUISettings => {
|
||||
const updatedUISettings = camelcaseKeys(newUISettings);
|
||||
|
||||
audioAlert.value = updatedUISettings.enableAudioAlerts;
|
||||
playAudioWhenTabIsInactive.value = !updatedUISettings.alwaysPlayAudioAlert;
|
||||
alertIfUnreadConversationExist.value =
|
||||
updatedUISettings.alertIfUnreadAssignedConversationExist;
|
||||
audioAlertConditions.value = [
|
||||
{
|
||||
id: 'audio1',
|
||||
label: t(`${i18nKeyPrefix}.CONDITIONS.CONDITION_ONE`),
|
||||
model: playAudioWhenTabIsInactive.value,
|
||||
value: 'tab_is_inactive',
|
||||
},
|
||||
{
|
||||
id: 'audio2',
|
||||
label: t(`${i18nKeyPrefix}.CONDITIONS.CONDITION_TWO`),
|
||||
model: alertIfUnreadConversationExist.value,
|
||||
value: 'conversations_are_read',
|
||||
},
|
||||
];
|
||||
alertTone.value = updatedUISettings.notificationTone || 'ding';
|
||||
};
|
||||
|
||||
watch(
|
||||
uiSettings,
|
||||
value => {
|
||||
initializeNotificationUISettings(value);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleAudioConfigChange = value => {
|
||||
updateUISettings(value);
|
||||
initializeAudioAlerts(currentUser.value);
|
||||
useAlert(t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('userNotificationSettings/get');
|
||||
});
|
||||
|
||||
const handAudioAlertChange = value => {
|
||||
audioAlert.value = value;
|
||||
handleAudioConfigChange({
|
||||
enable_audio_alerts: value,
|
||||
});
|
||||
};
|
||||
const handleAudioAlertConditions = (id, value) => {
|
||||
if (id === 'tab_is_inactive') {
|
||||
handleAudioConfigChange({
|
||||
always_play_audio_alert: !value,
|
||||
});
|
||||
} else if (id === 'conversations_are_read') {
|
||||
handleAudioConfigChange({
|
||||
alert_if_unread_assigned_conversation_exist: value,
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleAudioToneChange = value => {
|
||||
handleAudioConfigChange({ notification_tone: value });
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -100,27 +93,19 @@ export default {
|
||||
<div id="profile-settings-notifications" class="flex flex-col gap-6">
|
||||
<AudioAlertTone
|
||||
:value="alertTone"
|
||||
:label="
|
||||
$t(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.DEFAULT_TONE.TITLE'
|
||||
)
|
||||
"
|
||||
:label="$t(`${i18nKeyPrefix}.DEFAULT_TONE.TITLE`)"
|
||||
@change="handleAudioToneChange"
|
||||
/>
|
||||
|
||||
<AudioAlertEvent
|
||||
:label="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.TITLE')
|
||||
"
|
||||
:label="$t(`${i18nKeyPrefix}.ALERT_TYPE.TITLE`)"
|
||||
:value="audioAlert"
|
||||
@update="handAudioAlertChange"
|
||||
/>
|
||||
|
||||
<AudioAlertCondition
|
||||
:items="audioAlertConditions"
|
||||
:label="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.TITLE')
|
||||
"
|
||||
:label="$t(`${i18nKeyPrefix}.CONDITIONS.TITLE`)"
|
||||
@change="handleAudioAlertConditions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -36,17 +36,23 @@ export const NOTIFICATION_TYPES = [
|
||||
},
|
||||
];
|
||||
|
||||
export const EVENT_TYPES = {
|
||||
ASSIGNED: 'assigned',
|
||||
NOTME: 'notme',
|
||||
UNASSIGNED: 'unassigned',
|
||||
};
|
||||
|
||||
export const ALERT_EVENTS = [
|
||||
{
|
||||
value: 'none',
|
||||
label: 'none',
|
||||
value: EVENT_TYPES.ASSIGNED,
|
||||
label: 'assigned',
|
||||
},
|
||||
{
|
||||
value: 'mine',
|
||||
label: 'mine',
|
||||
value: EVENT_TYPES.UNASSIGNED,
|
||||
label: 'unassigned',
|
||||
},
|
||||
{
|
||||
value: 'all',
|
||||
label: 'all',
|
||||
value: EVENT_TYPES.NOTME,
|
||||
label: 'notme',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user