diff --git a/artifacts/api-server/src/routes/auth.ts b/artifacts/api-server/src/routes/auth.ts index 918e69eb..f92d01ba 100644 --- a/artifacts/api-server/src/routes/auth.ts +++ b/artifacts/api-server/src/routes/auth.ts @@ -66,7 +66,8 @@ function buildAuthUser( notificationSoundMeeting: user.notificationSoundMeeting, notifyOrdersEnabled: user.notifyOrdersEnabled, notifyMeetingsEnabled: user.notifyMeetingsEnabled, - vibrationEnabled: user.vibrationEnabled, + vibrationEnabledOrder: user.vibrationEnabledOrder, + vibrationEnabledMeeting: user.vibrationEnabledMeeting, notificationsMuted: user.notificationsMuted, notificationVolume: user.notificationVolume, roles, @@ -445,7 +446,8 @@ router.patch("/auth/me/notification-preferences", requireAuth, async (req, res): "notificationSoundMeeting", "notifyOrdersEnabled", "notifyMeetingsEnabled", - "vibrationEnabled", + "vibrationEnabledOrder", + "vibrationEnabledMeeting", "notificationsMuted", "notificationVolume", ] as const) { diff --git a/artifacts/tx-os/public/sounds/alert.wav b/artifacts/tx-os/public/sounds/alert.wav new file mode 100644 index 00000000..f8dd0aeb Binary files /dev/null and b/artifacts/tx-os/public/sounds/alert.wav differ diff --git a/artifacts/tx-os/public/sounds/beep.wav b/artifacts/tx-os/public/sounds/beep.wav new file mode 100644 index 00000000..32829e17 Binary files /dev/null and b/artifacts/tx-os/public/sounds/beep.wav differ diff --git a/artifacts/tx-os/public/sounds/bell.wav b/artifacts/tx-os/public/sounds/bell.wav new file mode 100644 index 00000000..85d1f01c Binary files /dev/null and b/artifacts/tx-os/public/sounds/bell.wav differ diff --git a/artifacts/tx-os/public/sounds/chime.wav b/artifacts/tx-os/public/sounds/chime.wav new file mode 100644 index 00000000..b73152ec Binary files /dev/null and b/artifacts/tx-os/public/sounds/chime.wav differ diff --git a/artifacts/tx-os/public/sounds/ding.wav b/artifacts/tx-os/public/sounds/ding.wav new file mode 100644 index 00000000..fdece0f1 Binary files /dev/null and b/artifacts/tx-os/public/sounds/ding.wav differ diff --git a/artifacts/tx-os/public/sounds/knock.wav b/artifacts/tx-os/public/sounds/knock.wav new file mode 100644 index 00000000..5765bd78 Binary files /dev/null and b/artifacts/tx-os/public/sounds/knock.wav differ diff --git a/artifacts/tx-os/public/sounds/pop.wav b/artifacts/tx-os/public/sounds/pop.wav new file mode 100644 index 00000000..6bb0fffc Binary files /dev/null and b/artifacts/tx-os/public/sounds/pop.wav differ diff --git a/artifacts/tx-os/public/sounds/soft.wav b/artifacts/tx-os/public/sounds/soft.wav new file mode 100644 index 00000000..c5631f14 Binary files /dev/null and b/artifacts/tx-os/public/sounds/soft.wav differ diff --git a/artifacts/tx-os/src/App.tsx b/artifacts/tx-os/src/App.tsx index 6698e08b..427a3398 100644 --- a/artifacts/tx-os/src/App.tsx +++ b/artifacts/tx-os/src/App.tsx @@ -5,6 +5,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { AuthProvider } from "@/contexts/AuthContext"; import { useNotificationsSocket } from "@/hooks/use-notifications-socket"; import { useAudioUnlock } from "@/hooks/use-audio-unlock"; +import { useAutoplayHint } from "@/hooks/use-autoplay-hint"; import { UpcomingMeetingAlert } from "@/components/executive-meetings/upcoming-meeting-alert"; import NotFound from "@/pages/not-found"; import LoginPage from "@/pages/login"; @@ -33,6 +34,7 @@ const queryClient = new QueryClient({ function NotificationsSocketBridge() { useNotificationsSocket(); useAudioUnlock(); + useAutoplayHint(); return null; } diff --git a/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx b/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx index 7e052c45..6b2e3833 100644 --- a/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx +++ b/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx @@ -41,6 +41,7 @@ import { } from "lucide-react"; import { hexToRgba, useAlertPrefs } from "@/lib/upcoming-alert-prefs"; import { formatTime as formatTimeLocale } from "@/lib/i18n-format"; +import { notificationPlayer } from "@/lib/notification-sounds"; const POSITION_KEY = "txos.upcomingMeetingAlert.position"; const ALERT_LEAD_MINUTES = 5; @@ -457,6 +458,22 @@ export function UpcomingMeetingAlert() { setExpanded(false); }, [eligibleId]); + // Play the meeting reminder sound exactly once per *new* eligible + // meeting (deduped by id) so polling refetches don't replay the chime + // every 30s. Honors the user's mute / per-channel toggle. + const playedRef = useRef>(new Set()); + useEffect(() => { + if (!eligibleId || !user) return; + if (playedRef.current.has(eligibleId)) return; + playedRef.current.add(eligibleId); + if (user.notificationsMuted) return; + if (!user.notifyMeetingsEnabled) return; + notificationPlayer.play(user.notificationSoundMeeting, { + volume: user.notificationVolume, + vibrate: user.vibrationEnabledMeeting, + }); + }, [eligibleId, user]); + if (!enabled || !eligible) return null; // User toggled the popup off in settings — render nothing. Detection // happens after `enabled/eligible` so the upstream "shown" recording diff --git a/artifacts/tx-os/src/components/notification-settings.tsx b/artifacts/tx-os/src/components/notification-settings.tsx index 9472cf98..cb3c1ada 100644 --- a/artifacts/tx-os/src/components/notification-settings.tsx +++ b/artifacts/tx-os/src/components/notification-settings.tsx @@ -7,7 +7,8 @@ import { type AuthUser, type NotificationSoundId, } from "@workspace/api-client-react"; -import { Bell, BellOff, Check, Volume2, Play } from "lucide-react"; +import { Bell, BellOff, Check, Volume2, VolumeX, Play } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; import { Popover, PopoverContent, @@ -21,7 +22,8 @@ type PrefsPatch = { notificationSoundMeeting?: NotificationSoundId; notifyOrdersEnabled?: boolean; notifyMeetingsEnabled?: boolean; - vibrationEnabled?: boolean; + vibrationEnabledOrder?: boolean; + vibrationEnabledMeeting?: boolean; notificationsMuted?: boolean; notificationVolume?: number; }; @@ -157,6 +159,42 @@ function Toggle({ ); } +/** + * One-tap mute toggle for the topbar (separate from the settings popover + * trigger). Shows a confirmation toast so the user knows the change took + * effect even though there's no visual focus shift. + */ +export function QuickMuteButton() { + const { t } = useTranslation(); + const { user } = useAuth(); + const { toast } = useToast(); + const apply = useUpdatePrefs(); + if (!user) return null; + const muted = user.notificationsMuted; + return ( + + ); +} + export function NotificationSettingsPicker() { const { t } = useTranslation(); const { user } = useAuth(); @@ -173,16 +211,9 @@ export function NotificationSettingsPicker() {