Task #389: Notification sounds + vibration with per-user customization
- DB: 7 new user prefs (sound per slot, per-type toggles, vibration, mute, volume). - API: PATCH /auth/me/notification-preferences with whitelisted partial update, integer guard for volume, and hydrated AuthUser response. AuthUser now exposes the 7 new fields. - Frontend: Web Audio synth library (8 sounds), settings popover with global mute/vibration/volume/per-type toggles, slot tabs, sound preview buttons, shift+click on bell for quick mute. Concurrency-safe optimistic updates with monotonic seq counter. RTL-correct toggle knob transforms. - Socket hook plays sound on notification_created (orders/meetings only) when tab is hidden, respecting global mute and per-type toggles. - Audio unlock hook mounted globally to satisfy autoplay restrictions. - AR/EN translations added. Pre-existing TS errors in executive-meetings.ts (font_settings) and pre-existing test failures in test workflow are unrelated to this task.
This commit is contained in:
@@ -54,6 +54,31 @@ export const LoginResponse = zod.object({
|
||||
clockHour12: zod.boolean().nullish(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean(),
|
||||
notificationSoundOrder: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundMeeting: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
vibrationEnabled: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
notificationVolume: zod.number(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
zod.object({
|
||||
@@ -142,6 +167,31 @@ export const GetMeResponse = zod.object({
|
||||
clockHour12: zod.boolean().nullish(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean(),
|
||||
notificationSoundOrder: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundMeeting: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
vibrationEnabled: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
notificationVolume: zod.number(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
zod.object({
|
||||
@@ -179,6 +229,31 @@ export const UpdateLanguageResponse = zod.object({
|
||||
clockHour12: zod.boolean().nullish(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean(),
|
||||
notificationSoundOrder: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundMeeting: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
vibrationEnabled: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
notificationVolume: zod.number(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
zod.object({
|
||||
@@ -223,6 +298,110 @@ export const UpdateClockStyleResponse = zod.object({
|
||||
clockHour12: zod.boolean().nullish(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean(),
|
||||
notificationSoundOrder: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundMeeting: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
vibrationEnabled: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
notificationVolume: zod.number(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
zod.object({
|
||||
id: zod.number(),
|
||||
name: zod.string(),
|
||||
descriptionAr: zod.string().nullish(),
|
||||
descriptionEn: zod.string().nullish(),
|
||||
}),
|
||||
),
|
||||
createdAt: zod.coerce.date(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Update notification sound, vibration & mute preferences
|
||||
*/
|
||||
export const updateNotificationPreferencesBodyNotificationVolumeMin = 0;
|
||||
export const updateNotificationPreferencesBodyNotificationVolumeMax = 100;
|
||||
|
||||
export const UpdateNotificationPreferencesBody = zod.object({
|
||||
notificationSoundOrder: zod
|
||||
.enum(["ding", "chime", "bell", "knock", "pop", "alert", "beep", "soft"])
|
||||
.optional(),
|
||||
notificationSoundMeeting: zod
|
||||
.enum(["ding", "chime", "bell", "knock", "pop", "alert", "beep", "soft"])
|
||||
.optional(),
|
||||
notifyOrdersEnabled: zod.boolean().optional(),
|
||||
notifyMeetingsEnabled: zod.boolean().optional(),
|
||||
vibrationEnabled: zod.boolean().optional(),
|
||||
notificationsMuted: zod.boolean().optional(),
|
||||
notificationVolume: zod
|
||||
.number()
|
||||
.min(updateNotificationPreferencesBodyNotificationVolumeMin)
|
||||
.max(updateNotificationPreferencesBodyNotificationVolumeMax)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const UpdateNotificationPreferencesResponse = zod.object({
|
||||
id: zod.number(),
|
||||
username: zod.string(),
|
||||
email: zod.string(),
|
||||
displayNameAr: zod.string().nullish(),
|
||||
displayNameEn: zod.string().nullish(),
|
||||
preferredLanguage: zod.string(),
|
||||
clockStyle: zod
|
||||
.union([
|
||||
zod
|
||||
.enum(["full", "digital", "digital-no-seconds", "analog", "minimal"])
|
||||
.describe("Per-user home-screen clock style preference"),
|
||||
zod.null(),
|
||||
])
|
||||
.optional(),
|
||||
clockHour12: zod.boolean().nullish(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean(),
|
||||
notificationSoundOrder: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundMeeting: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
vibrationEnabled: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
notificationVolume: zod.number(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
zod.object({
|
||||
@@ -265,6 +444,31 @@ export const UpdateClockHour12Response = zod.object({
|
||||
clockHour12: zod.boolean().nullish(),
|
||||
avatarUrl: zod.string().nullish(),
|
||||
isActive: zod.boolean(),
|
||||
notificationSoundOrder: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundMeeting: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
vibrationEnabled: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
notificationVolume: zod.number(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
zod.object({
|
||||
|
||||
Reference in New Issue
Block a user