diff --git a/web/public/static/sounds/beep.mp3 b/web/public/static/sounds/beep.mp3 new file mode 100644 index 0000000..d02e210 Binary files /dev/null and b/web/public/static/sounds/beep.mp3 differ diff --git a/web/public/static/sounds/juntos.mp3 b/web/public/static/sounds/juntos.mp3 new file mode 100644 index 0000000..aeadbb8 Binary files /dev/null and b/web/public/static/sounds/juntos.mp3 differ diff --git a/web/public/static/sounds/mixkit-correct-answer-tone.mp3 b/web/public/static/sounds/mixkit-correct-answer-tone.mp3 new file mode 100644 index 0000000..cdfd445 Binary files /dev/null and b/web/public/static/sounds/mixkit-correct-answer-tone.mp3 differ diff --git a/web/public/static/sounds/mixkit-long-pop.mp3 b/web/public/static/sounds/mixkit-long-pop.mp3 new file mode 100644 index 0000000..e650bb2 Binary files /dev/null and b/web/public/static/sounds/mixkit-long-pop.mp3 differ diff --git a/web/public/static/sounds/mixkit-message-pop-alert.mp3 b/web/public/static/sounds/mixkit-message-pop-alert.mp3 new file mode 100644 index 0000000..d8a83b7 Binary files /dev/null and b/web/public/static/sounds/mixkit-message-pop-alert.mp3 differ diff --git a/web/public/static/sounds/mixkit-software-interface-start.mp3 b/web/public/static/sounds/mixkit-software-interface-start.mp3 new file mode 100644 index 0000000..759057b Binary files /dev/null and b/web/public/static/sounds/mixkit-software-interface-start.mp3 differ diff --git a/web/public/static/sounds/pristine.mp3 b/web/public/static/sounds/pristine.mp3 new file mode 100644 index 0000000..ed3e308 Binary files /dev/null and b/web/public/static/sounds/pristine.mp3 differ diff --git a/web/src/app/NotificationManager.js b/web/src/app/Notifier.js similarity index 69% rename from web/src/app/NotificationManager.js rename to web/src/app/Notifier.js index 8834d2e..5db6f48 100644 --- a/web/src/app/NotificationManager.js +++ b/web/src/app/Notifier.js @@ -1,8 +1,8 @@ -import {formatMessage, formatTitleWithDefault, openUrl, topicShortUrl} from "./utils"; +import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicShortUrl} from "./utils"; import prefs from "./Prefs"; import subscriptionManager from "./SubscriptionManager"; -class NotificationManager { +class Notifier { async notify(subscriptionId, notification, onClickFallback) { const subscription = await subscriptionManager.get(subscriptionId); const shouldNotify = await this.shouldNotify(subscription, notification); @@ -13,7 +13,8 @@ class NotificationManager { const message = formatMessage(notification); const title = formatTitleWithDefault(notification, shortUrl); - console.log(`[NotificationManager, ${shortUrl}] Displaying notification ${notification.id}: ${message}`); + // Show notification + console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`); const n = new Notification(title, { body: message, icon: '/static/img/favicon.png' @@ -23,6 +24,17 @@ class NotificationManager { } else { n.onclick = onClickFallback; } + + // Play sound + const sound = await prefs.sound(); + if (sound && sound !== "none") { + try { + await playSound(sound); + } catch (e) { + console.log(`[Notifier, ${shortUrl}] Error playing audio`, e); + // FIXME show no sound allowed popup + } + } } granted() { @@ -48,5 +60,5 @@ class NotificationManager { } } -const notificationManager = new NotificationManager(); -export default notificationManager; +const notifier = new Notifier(); +export default notifier; diff --git a/web/src/app/Prefs.js b/web/src/app/Prefs.js index 359fbf6..6acc8f9 100644 --- a/web/src/app/Prefs.js +++ b/web/src/app/Prefs.js @@ -1,13 +1,13 @@ import db from "./db"; class Prefs { - async setSelectedSubscriptionId(selectedSubscriptionId) { - db.prefs.put({key: 'selectedSubscriptionId', value: selectedSubscriptionId}); + async setSound(sound) { + db.prefs.put({key: 'sound', value: sound.toString()}); } - async selectedSubscriptionId() { - const selectedSubscriptionId = await db.prefs.get('selectedSubscriptionId'); - return (selectedSubscriptionId) ? selectedSubscriptionId.value : ""; + async sound() { + const sound = await db.prefs.get('sound'); + return (sound) ? sound.value : "mixkit-correct-answer-tone"; } async setMinPriority(minPriority) { diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 17edce4..a64c3bd 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -41,7 +41,7 @@ class SubscriptionManager { if (exists) { return false; } - await db.notifications.add({ ...notification, subscriptionId }); + await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab await db.subscriptions.update(subscriptionId, { last: notification.id }); diff --git a/web/src/app/utils.js b/web/src/app/utils.js index c047ff7..c9b5b4c 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -121,6 +121,11 @@ export const subscriptionRoute = (subscription) => { return `/${subscription.topic}`; } +export const playSound = async (sound) => { + const audio = new Audio(`/static/sounds/${sound}.mp3`); + return audio.play(); +}; + // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch export async function* fetchLinesIterator(fileURL, headers) { const utf8Decoder = new TextDecoder('utf-8'); diff --git a/web/src/components/App.js b/web/src/components/App.js index 182df34..6db04aa 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -9,7 +9,7 @@ import theme from "./theme"; import connectionManager from "../app/ConnectionManager"; import Navigation from "./Navigation"; import ActionBar from "./ActionBar"; -import notificationManager from "../app/NotificationManager"; +import notifier from "../app/Notifier"; import NoTopics from "./NoTopics"; import Preferences from "./Preferences"; import {useLiveQuery} from "dexie-react-hooks"; @@ -26,6 +26,11 @@ import {subscriptionRoute} from "../app/utils"; // TODO sound // TODO "copy url" toast // TODO "copy link url" button +// TODO races when two tabs are open +// TODO sound mentions +// https://notificationsounds.com/message-tones/pristine-609 +// https://notificationsounds.com/message-tones/juntos-607 +// https://notificationsounds.com/notification-sounds/beep-472 const App = () => { return ( @@ -40,7 +45,7 @@ const App = () => { const Root = () => { const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); - const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted()); + const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted()); const navigate = useNavigate(); const location = useLocation(); const users = useLiveQuery(() => userManager.all()); @@ -54,7 +59,7 @@ const Root = () => { }; const handleRequestPermission = () => { - notificationManager.maybeRequestPermission(granted => setNotificationsGranted(granted)); + notifier.maybeRequestPermission(granted => setNotificationsGranted(granted)); }; useEffect(() => { @@ -68,7 +73,7 @@ const Root = () => { const added = await subscriptionManager.addNotification(subscriptionId, notification); if (added) { const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); // FIXME - await notificationManager.notify(subscriptionId, notification, defaultClickAction) + await notifier.notify(subscriptionId, notification, defaultClickAction) } } catch (e) { console.error(`[App] Error handling notification`, e); diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index 6f5ac0e..a9b4e87 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -19,6 +19,7 @@ import {Paragraph} from "./styles"; import EditIcon from '@mui/icons-material/Edit'; import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import Container from "@mui/material/Container"; import TextField from "@mui/material/TextField"; import MenuItem from "@mui/material/MenuItem"; @@ -31,6 +32,7 @@ import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; import DialogActions from "@mui/material/DialogActions"; import userManager from "../app/UserManager"; +import {playSound} from "../app/utils"; const Preferences = () => { return ( @@ -50,6 +52,7 @@ const Notifications = () => { Notifications + @@ -57,8 +60,40 @@ const Notifications = () => { ); }; + +const Sound = () => { + const sound = useLiveQuery(async () => prefs.sound()); + const handleChange = async (ev) => { + await prefs.setSound(ev.target.value); + } + if (!sound) { + return null; // While loading + } + return ( + +
+ + + + playSound(sound)} disabled={sound === "none"}> + + +
+
+ ) +}; + const MinPriority = () => { - const minPriority = useLiveQuery(() => prefs.minPriority()); + const minPriority = useLiveQuery(async () => prefs.minPriority()); const handleChange = async (ev) => { await prefs.setMinPriority(ev.target.value); }