feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention model so recipients can't miss them. Changes: - DB: add `notification_sound_note` (default "knock"), `notify_notes_enabled`, `vibration_enabled_note` to users schema; pushed via drizzle. - API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody; regenerated codegen. - Auth route: include new fields in buildAuthUser + PATCH allowlist. - Socket hook: play notification sound + vibrate on `note_received`, gated by mute/notifyNotesEnabled/socket warmup, with playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef). - IncomingNotePopupContext: enqueue() now returns boolean acceptance so the socket hook can suppress sound on own-note/duplicate. - Popup visual: z-[110], ring-4 amber, shadow-2xl, animate-in zoom, avatar animate-ping pulse. - Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot tab (grid-cols-3) with enabled/vibration toggles + sound picker. - Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote. Pre-existing typecheck errors in executive-meetings.ts (font settings scope) are unrelated to this task.
This commit is contained in:
@@ -64,10 +64,13 @@ function buildAuthUser(
|
||||
isActive: user.isActive,
|
||||
notificationSoundOrder: user.notificationSoundOrder,
|
||||
notificationSoundMeeting: user.notificationSoundMeeting,
|
||||
notificationSoundNote: user.notificationSoundNote,
|
||||
notifyOrdersEnabled: user.notifyOrdersEnabled,
|
||||
notifyMeetingsEnabled: user.notifyMeetingsEnabled,
|
||||
notifyNotesEnabled: user.notifyNotesEnabled,
|
||||
vibrationEnabledOrder: user.vibrationEnabledOrder,
|
||||
vibrationEnabledMeeting: user.vibrationEnabledMeeting,
|
||||
vibrationEnabledNote: user.vibrationEnabledNote,
|
||||
notificationsMuted: user.notificationsMuted,
|
||||
roles,
|
||||
groups,
|
||||
@@ -435,10 +438,13 @@ router.patch("/auth/me/notification-preferences", requireAuth, async (req, res):
|
||||
for (const k of [
|
||||
"notificationSoundOrder",
|
||||
"notificationSoundMeeting",
|
||||
"notificationSoundNote",
|
||||
"notifyOrdersEnabled",
|
||||
"notifyMeetingsEnabled",
|
||||
"notifyNotesEnabled",
|
||||
"vibrationEnabledOrder",
|
||||
"vibrationEnabledMeeting",
|
||||
"vibrationEnabledNote",
|
||||
"notificationsMuted",
|
||||
] as const) {
|
||||
if (parsed.data[k] !== undefined) updates[k] = parsed.data[k];
|
||||
|
||||
@@ -69,21 +69,27 @@ export function IncomingNotePopup() {
|
||||
<AlertDialogContent
|
||||
dir={isRtl ? "rtl" : "ltr"}
|
||||
data-testid="incoming-note-popup"
|
||||
className="max-w-md"
|
||||
className="max-w-md z-[110] ring-4 ring-amber-400/70 shadow-2xl shadow-amber-500/30 animate-in zoom-in-95 fade-in-0 duration-200"
|
||||
>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-3">
|
||||
{current.sender?.avatarUrl ? (
|
||||
<img
|
||||
src={current.sender.avatarUrl}
|
||||
alt=""
|
||||
className="h-9 w-9 rounded-full object-cover shrink-0"
|
||||
<span className="relative shrink-0">
|
||||
<span
|
||||
className="absolute inset-0 rounded-full bg-amber-400/60 animate-ping"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-9 w-9 rounded-full bg-amber-200 text-amber-900 font-semibold flex items-center justify-center shrink-0">
|
||||
{avatarInitial(current.sender, i18n.language)}
|
||||
</div>
|
||||
)}
|
||||
{current.sender?.avatarUrl ? (
|
||||
<img
|
||||
src={current.sender.avatarUrl}
|
||||
alt=""
|
||||
className="relative h-9 w-9 rounded-full object-cover ring-2 ring-amber-400"
|
||||
/>
|
||||
) : (
|
||||
<div className="relative h-9 w-9 rounded-full bg-amber-200 text-amber-900 font-semibold flex items-center justify-center ring-2 ring-amber-400">
|
||||
{avatarInitial(current.sender, i18n.language)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-base">{heading}</span>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="sr-only">
|
||||
|
||||
@@ -20,10 +20,13 @@ import { useAuth } from "@/contexts/AuthContext";
|
||||
type PrefsPatch = {
|
||||
notificationSoundOrder?: NotificationSoundId;
|
||||
notificationSoundMeeting?: NotificationSoundId;
|
||||
notificationSoundNote?: NotificationSoundId;
|
||||
notifyOrdersEnabled?: boolean;
|
||||
notifyMeetingsEnabled?: boolean;
|
||||
notifyNotesEnabled?: boolean;
|
||||
vibrationEnabledOrder?: boolean;
|
||||
vibrationEnabledMeeting?: boolean;
|
||||
vibrationEnabledNote?: boolean;
|
||||
notificationsMuted?: boolean;
|
||||
};
|
||||
|
||||
@@ -69,7 +72,51 @@ export function useUpdatePrefs() {
|
||||
};
|
||||
}
|
||||
|
||||
type Slot = "order" | "meeting";
|
||||
type Slot = "order" | "meeting" | "note";
|
||||
|
||||
const SLOT_KEYS: Record<
|
||||
Slot,
|
||||
{
|
||||
enabledKey: keyof Pick<
|
||||
AuthUser,
|
||||
"notifyOrdersEnabled" | "notifyMeetingsEnabled" | "notifyNotesEnabled"
|
||||
>;
|
||||
soundKey: keyof Pick<
|
||||
AuthUser,
|
||||
| "notificationSoundOrder"
|
||||
| "notificationSoundMeeting"
|
||||
| "notificationSoundNote"
|
||||
>;
|
||||
vibrateKey: keyof Pick<
|
||||
AuthUser,
|
||||
"vibrationEnabledOrder" | "vibrationEnabledMeeting" | "vibrationEnabledNote"
|
||||
>;
|
||||
enabledLabel: string;
|
||||
vibrateLabel: string;
|
||||
}
|
||||
> = {
|
||||
order: {
|
||||
enabledKey: "notifyOrdersEnabled",
|
||||
soundKey: "notificationSoundOrder",
|
||||
vibrateKey: "vibrationEnabledOrder",
|
||||
enabledLabel: "notifSettings.ordersEnabled",
|
||||
vibrateLabel: "notifSettings.vibrationOrder",
|
||||
},
|
||||
meeting: {
|
||||
enabledKey: "notifyMeetingsEnabled",
|
||||
soundKey: "notificationSoundMeeting",
|
||||
vibrateKey: "vibrationEnabledMeeting",
|
||||
enabledLabel: "notifSettings.meetingsEnabled",
|
||||
vibrateLabel: "notifSettings.vibrationMeeting",
|
||||
},
|
||||
note: {
|
||||
enabledKey: "notifyNotesEnabled",
|
||||
soundKey: "notificationSoundNote",
|
||||
vibrateKey: "vibrationEnabledNote",
|
||||
enabledLabel: "notifSettings.notesEnabled",
|
||||
vibrateLabel: "notifSettings.vibrationNote",
|
||||
},
|
||||
};
|
||||
|
||||
function SoundList({
|
||||
current,
|
||||
@@ -201,6 +248,7 @@ export function NotificationSettingsPicker() {
|
||||
|
||||
if (!user) return null;
|
||||
const muted = user.notificationsMuted;
|
||||
const cfg = SLOT_KEYS[slot];
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -232,10 +280,10 @@ export function NotificationSettingsPicker() {
|
||||
<div className="h-px bg-foreground/10 mx-1 my-1" />
|
||||
|
||||
<div
|
||||
className="grid grid-cols-2 gap-1 mx-1 mb-1 p-1 rounded-lg bg-foreground/5"
|
||||
className="grid grid-cols-3 gap-1 mx-1 mb-1 p-1 rounded-lg bg-foreground/5"
|
||||
role="group"
|
||||
>
|
||||
{(["order", "meeting"] as const).map((s) => {
|
||||
{(["order", "meeting", "note"] as const).map((s) => {
|
||||
const isActive = s === slot;
|
||||
return (
|
||||
<button
|
||||
@@ -256,58 +304,26 @@ export function NotificationSettingsPicker() {
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
active={
|
||||
slot === "order"
|
||||
? user.notifyOrdersEnabled
|
||||
: user.notifyMeetingsEnabled
|
||||
}
|
||||
active={user[cfg.enabledKey]}
|
||||
onClick={() =>
|
||||
apply(
|
||||
slot === "order"
|
||||
? { notifyOrdersEnabled: !user.notifyOrdersEnabled }
|
||||
: { notifyMeetingsEnabled: !user.notifyMeetingsEnabled },
|
||||
)
|
||||
apply({ [cfg.enabledKey]: !user[cfg.enabledKey] } as PrefsPatch)
|
||||
}
|
||||
label={t(
|
||||
slot === "order"
|
||||
? "notifSettings.ordersEnabled"
|
||||
: "notifSettings.meetingsEnabled",
|
||||
)}
|
||||
label={t(cfg.enabledLabel)}
|
||||
/>
|
||||
<Toggle
|
||||
active={
|
||||
slot === "order"
|
||||
? user.vibrationEnabledOrder
|
||||
: user.vibrationEnabledMeeting
|
||||
}
|
||||
active={user[cfg.vibrateKey]}
|
||||
onClick={() =>
|
||||
apply(
|
||||
slot === "order"
|
||||
? { vibrationEnabledOrder: !user.vibrationEnabledOrder }
|
||||
: { vibrationEnabledMeeting: !user.vibrationEnabledMeeting },
|
||||
)
|
||||
apply({ [cfg.vibrateKey]: !user[cfg.vibrateKey] } as PrefsPatch)
|
||||
}
|
||||
label={t(
|
||||
slot === "order"
|
||||
? "notifSettings.vibrationOrder"
|
||||
: "notifSettings.vibrationMeeting",
|
||||
)}
|
||||
label={t(cfg.vibrateLabel)}
|
||||
/>
|
||||
|
||||
<div className="h-px bg-foreground/10 mx-1 my-1" />
|
||||
|
||||
<SoundList
|
||||
current={
|
||||
slot === "order"
|
||||
? user.notificationSoundOrder
|
||||
: user.notificationSoundMeeting
|
||||
}
|
||||
current={user[cfg.soundKey]}
|
||||
onPick={(id) =>
|
||||
apply(
|
||||
slot === "order"
|
||||
? { notificationSoundOrder: id }
|
||||
: { notificationSoundMeeting: id },
|
||||
)
|
||||
apply({ [cfg.soundKey]: id } as PrefsPatch)
|
||||
}
|
||||
/>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -19,7 +19,17 @@ export type { IncomingNotePayload } from "@/lib/incoming-note-queue";
|
||||
interface IncomingNotePopupContextValue {
|
||||
current: IncomingNotePayload | null;
|
||||
queueLength: number;
|
||||
enqueue: (payload: IncomingNotePayload, currentUserId: number | null) => void;
|
||||
/**
|
||||
* Adds the payload to the FIFO popup queue. Returns true if the
|
||||
* payload was actually queued, false if it was suppressed (sender's
|
||||
* own note, duplicate noteId, or malformed). Callers use the boolean
|
||||
* to gate side-effects like the chime so we don't beep for our own
|
||||
* outgoing notes or for replayed socket events.
|
||||
*/
|
||||
enqueue: (
|
||||
payload: IncomingNotePayload,
|
||||
currentUserId: number | null,
|
||||
) => boolean;
|
||||
dismiss: (noteId: number) => void;
|
||||
clear: () => void;
|
||||
}
|
||||
@@ -40,8 +50,18 @@ export function IncomingNotePopupProvider({ children }: { children: ReactNode })
|
||||
}, [userId]);
|
||||
|
||||
const enqueue = useCallback(
|
||||
(payload: IncomingNotePayload, currentUserId: number | null) => {
|
||||
setQueue((prev) => applyEnqueue(prev, payload, currentUserId));
|
||||
(payload: IncomingNotePayload, currentUserId: number | null): boolean => {
|
||||
// The state updater runs synchronously inside setQueue, so the
|
||||
// captured `accepted` is populated before this function returns —
|
||||
// safe to read on the next line. Mirrors applyEnqueue's exact
|
||||
// suppression rules without duplicating them.
|
||||
let accepted = false;
|
||||
setQueue((prev) => {
|
||||
const next = applyEnqueue(prev, payload, currentUserId);
|
||||
accepted = next !== prev;
|
||||
return next;
|
||||
});
|
||||
return accepted;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -38,6 +38,11 @@ export function useNotificationsSocket() {
|
||||
enqueueNotePopupRef.current = enqueueNotePopup;
|
||||
const currentUserIdRef = useRef<number | null>(user?.id ?? null);
|
||||
currentUserIdRef.current = user?.id ?? null;
|
||||
// Dedupe note chimes by noteId so a socket reconnect that replays the
|
||||
// same `note_received` event (or a duplicate emit from the server)
|
||||
// doesn't beep twice for the same incoming note. Mirrors the
|
||||
// `playedRef` pattern used by upcoming-meeting-alert.
|
||||
const playedNoteIdsRef = useRef<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
@@ -120,6 +125,7 @@ export function useNotificationsSocket() {
|
||||
socket.on("note_received", (payload?: Partial<IncomingNotePayload>) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["notes"] });
|
||||
const inWarmup = nowMs() - connectedAtRef.current < SOCKET_WARMUP_MS;
|
||||
let enqueued = false;
|
||||
if (
|
||||
payload &&
|
||||
typeof payload.noteId === "number" &&
|
||||
@@ -135,9 +141,29 @@ export function useNotificationsSocket() {
|
||||
senderUserId: payload.senderUserId,
|
||||
sender: payload.sender ?? null,
|
||||
};
|
||||
enqueueNotePopupRef.current(full, currentUserIdRef.current);
|
||||
enqueued = enqueueNotePopupRef.current(
|
||||
full,
|
||||
currentUserIdRef.current,
|
||||
);
|
||||
}
|
||||
if (inWarmup) return;
|
||||
// Sound + vibration for incoming notes — gated by global mute and
|
||||
// the per-channel notes toggle. Deduped by noteId so a socket
|
||||
// reconnect that replays the same event doesn't re-chime. Only
|
||||
// plays when the popup actually enqueued (i.e. recipient !== sender).
|
||||
if (
|
||||
enqueued &&
|
||||
payload &&
|
||||
typeof payload.noteId === "number" &&
|
||||
!user.notificationsMuted &&
|
||||
user.notifyNotesEnabled &&
|
||||
!playedNoteIdsRef.current.has(payload.noteId)
|
||||
) {
|
||||
playedNoteIdsRef.current.add(payload.noteId);
|
||||
notificationPlayer.play(user.notificationSoundNote, {
|
||||
vibrate: user.vibrationEnabledNote,
|
||||
});
|
||||
}
|
||||
toast({
|
||||
title: i18n.t("notes.toast.received.title"),
|
||||
description: i18n.t("notes.toast.received.desc"),
|
||||
|
||||
@@ -28,6 +28,25 @@ export function applyEnqueue(
|
||||
return [...queue, payload];
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate: would this payload actually land in the queue, or is it
|
||||
* silently dropped (sender's own note, duplicate, malformed)? Used by
|
||||
* the socket layer to decide whether to play the chime. Mirrors the
|
||||
* suppression rules in {@link applyEnqueue} so the two stay in sync.
|
||||
*/
|
||||
export function shouldEnqueueNote(
|
||||
queue: IncomingNotePayload[],
|
||||
payload: IncomingNotePayload,
|
||||
currentUserId: number | null,
|
||||
): boolean {
|
||||
if (!payload || typeof payload.noteId !== "number") return false;
|
||||
if (currentUserId != null && payload.senderUserId === currentUserId) {
|
||||
return false;
|
||||
}
|
||||
if (queue.some((p) => p.noteId === payload.noteId)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the head of the queue if `noteId` matches it; otherwise filter it
|
||||
* out from anywhere it appears. Pure for testability.
|
||||
|
||||
@@ -311,8 +311,10 @@
|
||||
"volume": "مستوى الصوت",
|
||||
"ordersEnabled": "صوت للطلبات الجديدة",
|
||||
"meetingsEnabled": "صوت لتذكيرات الاجتماعات",
|
||||
"notesEnabled": "صوت للملاحظات الواردة",
|
||||
"vibrationOrder": "اهتزاز عند الطلبات الجديدة",
|
||||
"vibrationMeeting": "اهتزاز عند تذكيرات الاجتماعات",
|
||||
"vibrationNote": "اهتزاز عند الملاحظات الواردة",
|
||||
"preview": "تجربة",
|
||||
"quickMute": "كتم الإشعارات",
|
||||
"quickUnmute": "إلغاء كتم الإشعارات",
|
||||
@@ -322,7 +324,8 @@
|
||||
"autoplayHint": "انقر في أي مكان لتفعيل أصوات الإشعارات.",
|
||||
"slot": {
|
||||
"order": "الطلبات",
|
||||
"meeting": "الاجتماعات"
|
||||
"meeting": "الاجتماعات",
|
||||
"note": "الملاحظات"
|
||||
},
|
||||
"sounds": {
|
||||
"ding": "رنين",
|
||||
|
||||
@@ -314,8 +314,10 @@
|
||||
"volume": "Volume",
|
||||
"ordersEnabled": "Sound for new orders",
|
||||
"meetingsEnabled": "Sound for meeting reminders",
|
||||
"notesEnabled": "Sound for incoming notes",
|
||||
"vibrationOrder": "Vibrate on new orders",
|
||||
"vibrationMeeting": "Vibrate on meeting reminders",
|
||||
"vibrationNote": "Vibrate on incoming notes",
|
||||
"preview": "Preview",
|
||||
"quickMute": "Mute notifications",
|
||||
"quickUnmute": "Unmute notifications",
|
||||
@@ -325,7 +327,8 @@
|
||||
"autoplayHint": "Click anywhere on the page to enable notification sounds.",
|
||||
"slot": {
|
||||
"order": "Orders",
|
||||
"meeting": "Meetings"
|
||||
"meeting": "Meetings",
|
||||
"note": "Notes"
|
||||
},
|
||||
"sounds": {
|
||||
"ding": "Ding",
|
||||
|
||||
@@ -113,10 +113,13 @@ export const NotificationSoundId = {
|
||||
export interface UpdateNotificationPreferencesBody {
|
||||
notificationSoundOrder?: NotificationSoundId;
|
||||
notificationSoundMeeting?: NotificationSoundId;
|
||||
notificationSoundNote?: NotificationSoundId;
|
||||
notifyOrdersEnabled?: boolean;
|
||||
notifyMeetingsEnabled?: boolean;
|
||||
notifyNotesEnabled?: boolean;
|
||||
vibrationEnabledOrder?: boolean;
|
||||
vibrationEnabledMeeting?: boolean;
|
||||
vibrationEnabledNote?: boolean;
|
||||
notificationsMuted?: boolean;
|
||||
}
|
||||
|
||||
@@ -154,10 +157,13 @@ export interface AuthUser {
|
||||
isActive: boolean;
|
||||
notificationSoundOrder: NotificationSoundId;
|
||||
notificationSoundMeeting: NotificationSoundId;
|
||||
notificationSoundNote: NotificationSoundId;
|
||||
notifyOrdersEnabled: boolean;
|
||||
notifyMeetingsEnabled: boolean;
|
||||
notifyNotesEnabled: boolean;
|
||||
vibrationEnabledOrder: boolean;
|
||||
vibrationEnabledMeeting: boolean;
|
||||
vibrationEnabledNote: boolean;
|
||||
notificationsMuted: boolean;
|
||||
roles: string[];
|
||||
groups: GroupSummary[];
|
||||
|
||||
@@ -3209,14 +3209,20 @@ components:
|
||||
$ref: "#/components/schemas/NotificationSoundId"
|
||||
notificationSoundMeeting:
|
||||
$ref: "#/components/schemas/NotificationSoundId"
|
||||
notificationSoundNote:
|
||||
$ref: "#/components/schemas/NotificationSoundId"
|
||||
notifyOrdersEnabled:
|
||||
type: boolean
|
||||
notifyMeetingsEnabled:
|
||||
type: boolean
|
||||
notifyNotesEnabled:
|
||||
type: boolean
|
||||
vibrationEnabledOrder:
|
||||
type: boolean
|
||||
vibrationEnabledMeeting:
|
||||
type: boolean
|
||||
vibrationEnabledNote:
|
||||
type: boolean
|
||||
notificationsMuted:
|
||||
type: boolean
|
||||
|
||||
@@ -3258,14 +3264,20 @@ components:
|
||||
$ref: "#/components/schemas/NotificationSoundId"
|
||||
notificationSoundMeeting:
|
||||
$ref: "#/components/schemas/NotificationSoundId"
|
||||
notificationSoundNote:
|
||||
$ref: "#/components/schemas/NotificationSoundId"
|
||||
notifyOrdersEnabled:
|
||||
type: boolean
|
||||
notifyMeetingsEnabled:
|
||||
type: boolean
|
||||
notifyNotesEnabled:
|
||||
type: boolean
|
||||
vibrationEnabledOrder:
|
||||
type: boolean
|
||||
vibrationEnabledMeeting:
|
||||
type: boolean
|
||||
vibrationEnabledNote:
|
||||
type: boolean
|
||||
notificationsMuted:
|
||||
type: boolean
|
||||
roles:
|
||||
@@ -3287,10 +3299,13 @@ components:
|
||||
- isActive
|
||||
- notificationSoundOrder
|
||||
- notificationSoundMeeting
|
||||
- notificationSoundNote
|
||||
- notifyOrdersEnabled
|
||||
- notifyMeetingsEnabled
|
||||
- notifyNotesEnabled
|
||||
- vibrationEnabledOrder
|
||||
- vibrationEnabledMeeting
|
||||
- vibrationEnabledNote
|
||||
- notificationsMuted
|
||||
- roles
|
||||
- groups
|
||||
|
||||
@@ -74,10 +74,22 @@ export const LoginResponse = zod.object({
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundNote: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
notifyNotesEnabled: zod.boolean(),
|
||||
vibrationEnabledOrder: zod.boolean(),
|
||||
vibrationEnabledMeeting: zod.boolean(),
|
||||
vibrationEnabledNote: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
@@ -187,10 +199,22 @@ export const GetMeResponse = zod.object({
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundNote: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
notifyNotesEnabled: zod.boolean(),
|
||||
vibrationEnabledOrder: zod.boolean(),
|
||||
vibrationEnabledMeeting: zod.boolean(),
|
||||
vibrationEnabledNote: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
@@ -249,10 +273,22 @@ export const UpdateLanguageResponse = zod.object({
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundNote: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
notifyNotesEnabled: zod.boolean(),
|
||||
vibrationEnabledOrder: zod.boolean(),
|
||||
vibrationEnabledMeeting: zod.boolean(),
|
||||
vibrationEnabledNote: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
@@ -318,10 +354,22 @@ export const UpdateClockStyleResponse = zod.object({
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundNote: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
notifyNotesEnabled: zod.boolean(),
|
||||
vibrationEnabledOrder: zod.boolean(),
|
||||
vibrationEnabledMeeting: zod.boolean(),
|
||||
vibrationEnabledNote: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
@@ -345,10 +393,15 @@ export const UpdateNotificationPreferencesBody = zod.object({
|
||||
notificationSoundMeeting: zod
|
||||
.enum(["ding", "chime", "bell", "knock", "pop", "alert", "beep", "soft"])
|
||||
.optional(),
|
||||
notificationSoundNote: zod
|
||||
.enum(["ding", "chime", "bell", "knock", "pop", "alert", "beep", "soft"])
|
||||
.optional(),
|
||||
notifyOrdersEnabled: zod.boolean().optional(),
|
||||
notifyMeetingsEnabled: zod.boolean().optional(),
|
||||
notifyNotesEnabled: zod.boolean().optional(),
|
||||
vibrationEnabledOrder: zod.boolean().optional(),
|
||||
vibrationEnabledMeeting: zod.boolean().optional(),
|
||||
vibrationEnabledNote: zod.boolean().optional(),
|
||||
notificationsMuted: zod.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -390,10 +443,22 @@ export const UpdateNotificationPreferencesResponse = zod.object({
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundNote: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
notifyNotesEnabled: zod.boolean(),
|
||||
vibrationEnabledOrder: zod.boolean(),
|
||||
vibrationEnabledMeeting: zod.boolean(),
|
||||
vibrationEnabledNote: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
@@ -457,10 +522,22 @@ export const UpdateClockHour12Response = zod.object({
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notificationSoundNote: zod.enum([
|
||||
"ding",
|
||||
"chime",
|
||||
"bell",
|
||||
"knock",
|
||||
"pop",
|
||||
"alert",
|
||||
"beep",
|
||||
"soft",
|
||||
]),
|
||||
notifyOrdersEnabled: zod.boolean(),
|
||||
notifyMeetingsEnabled: zod.boolean(),
|
||||
notifyNotesEnabled: zod.boolean(),
|
||||
vibrationEnabledOrder: zod.boolean(),
|
||||
vibrationEnabledMeeting: zod.boolean(),
|
||||
vibrationEnabledNote: zod.boolean(),
|
||||
notificationsMuted: zod.boolean(),
|
||||
roles: zod.array(zod.string()),
|
||||
groups: zod.array(
|
||||
|
||||
@@ -16,10 +16,13 @@ export const usersTable = pgTable("users", {
|
||||
isActive: boolean("is_active").notNull().default(true),
|
||||
notificationSoundOrder: varchar("notification_sound_order", { length: 30 }).notNull().default("ding"),
|
||||
notificationSoundMeeting: varchar("notification_sound_meeting", { length: 30 }).notNull().default("chime"),
|
||||
notificationSoundNote: varchar("notification_sound_note", { length: 30 }).notNull().default("knock"),
|
||||
notifyOrdersEnabled: boolean("notify_orders_enabled").notNull().default(true),
|
||||
notifyMeetingsEnabled: boolean("notify_meetings_enabled").notNull().default(true),
|
||||
notifyNotesEnabled: boolean("notify_notes_enabled").notNull().default(true),
|
||||
vibrationEnabledOrder: boolean("vibration_enabled_order").notNull().default(true),
|
||||
vibrationEnabledMeeting: boolean("vibration_enabled_meeting").notNull().default(true),
|
||||
vibrationEnabledNote: boolean("vibration_enabled_note").notNull().default(true),
|
||||
notificationsMuted: boolean("notifications_muted").notNull().default(false),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()),
|
||||
|
||||
Reference in New Issue
Block a user