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:
@@ -96,6 +96,34 @@ export interface UpdateClockStyleBody {
|
||||
clockStyle: ClockStyle | null;
|
||||
}
|
||||
|
||||
export type NotificationSoundId =
|
||||
(typeof NotificationSoundId)[keyof typeof NotificationSoundId];
|
||||
|
||||
export const NotificationSoundId = {
|
||||
ding: "ding",
|
||||
chime: "chime",
|
||||
bell: "bell",
|
||||
knock: "knock",
|
||||
pop: "pop",
|
||||
alert: "alert",
|
||||
beep: "beep",
|
||||
soft: "soft",
|
||||
} as const;
|
||||
|
||||
export interface UpdateNotificationPreferencesBody {
|
||||
notificationSoundOrder?: NotificationSoundId;
|
||||
notificationSoundMeeting?: NotificationSoundId;
|
||||
notifyOrdersEnabled?: boolean;
|
||||
notifyMeetingsEnabled?: boolean;
|
||||
vibrationEnabled?: boolean;
|
||||
notificationsMuted?: boolean;
|
||||
/**
|
||||
* @minimum 0
|
||||
* @maximum 100
|
||||
*/
|
||||
notificationVolume?: number;
|
||||
}
|
||||
|
||||
export interface UpdateClockHour12Body {
|
||||
/**
|
||||
* Whether to use 12-hour (AM/PM) clock format; null clears and uses the default (24-hour)
|
||||
@@ -128,6 +156,13 @@ export interface AuthUser {
|
||||
/** @nullable */
|
||||
avatarUrl?: string | null;
|
||||
isActive: boolean;
|
||||
notificationSoundOrder: NotificationSoundId;
|
||||
notificationSoundMeeting: NotificationSoundId;
|
||||
notifyOrdersEnabled: boolean;
|
||||
notifyMeetingsEnabled: boolean;
|
||||
vibrationEnabled: boolean;
|
||||
notificationsMuted: boolean;
|
||||
notificationVolume: number;
|
||||
roles: string[];
|
||||
groups: GroupSummary[];
|
||||
createdAt: string;
|
||||
|
||||
@@ -108,6 +108,7 @@ import type {
|
||||
UpdateGroupBody,
|
||||
UpdateLanguageBody,
|
||||
UpdateMyAppOrderBody,
|
||||
UpdateNotificationPreferencesBody,
|
||||
UpdateRoleBody,
|
||||
UpdateServiceBody,
|
||||
UpdateServiceOrderStatusBody,
|
||||
@@ -1036,6 +1037,94 @@ export const useUpdateClockStyle = <
|
||||
return useMutation(getUpdateClockStyleMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Update notification sound, vibration & mute preferences
|
||||
*/
|
||||
export const getUpdateNotificationPreferencesUrl = () => {
|
||||
return `/api/auth/me/notification-preferences`;
|
||||
};
|
||||
|
||||
export const updateNotificationPreferences = async (
|
||||
updateNotificationPreferencesBody: UpdateNotificationPreferencesBody,
|
||||
options?: RequestInit,
|
||||
): Promise<AuthUser> => {
|
||||
return customFetch<AuthUser>(getUpdateNotificationPreferencesUrl(), {
|
||||
...options,
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(updateNotificationPreferencesBody),
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateNotificationPreferencesMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateNotificationPreferences>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateNotificationPreferencesBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateNotificationPreferences>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateNotificationPreferencesBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["updateNotificationPreferences"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateNotificationPreferences>>,
|
||||
{ data: BodyType<UpdateNotificationPreferencesBody> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return updateNotificationPreferences(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateNotificationPreferencesMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateNotificationPreferences>>
|
||||
>;
|
||||
export type UpdateNotificationPreferencesMutationBody =
|
||||
BodyType<UpdateNotificationPreferencesBody>;
|
||||
export type UpdateNotificationPreferencesMutationError =
|
||||
ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Update notification sound, vibration & mute preferences
|
||||
*/
|
||||
export const useUpdateNotificationPreferences = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateNotificationPreferences>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateNotificationPreferencesBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateNotificationPreferences>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateNotificationPreferencesBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateNotificationPreferencesMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Update preferred 12/24-hour clock format
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user