Support sounds

pull/149/head
Philipp Heckel 2022-03-06 00:02:27 -05:00
parent 09b128f27a
commit dc7ca6e405
13 changed files with 73 additions and 16 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,8 +1,8 @@
import {formatMessage, formatTitleWithDefault, openUrl, topicShortUrl} from "./utils"; import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicShortUrl} from "./utils";
import prefs from "./Prefs"; import prefs from "./Prefs";
import subscriptionManager from "./SubscriptionManager"; import subscriptionManager from "./SubscriptionManager";
class NotificationManager { class Notifier {
async notify(subscriptionId, notification, onClickFallback) { async notify(subscriptionId, notification, onClickFallback) {
const subscription = await subscriptionManager.get(subscriptionId); const subscription = await subscriptionManager.get(subscriptionId);
const shouldNotify = await this.shouldNotify(subscription, notification); const shouldNotify = await this.shouldNotify(subscription, notification);
@ -13,7 +13,8 @@ class NotificationManager {
const message = formatMessage(notification); const message = formatMessage(notification);
const title = formatTitleWithDefault(notification, shortUrl); 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, { const n = new Notification(title, {
body: message, body: message,
icon: '/static/img/favicon.png' icon: '/static/img/favicon.png'
@ -23,6 +24,17 @@ class NotificationManager {
} else { } else {
n.onclick = onClickFallback; 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() { granted() {
@ -48,5 +60,5 @@ class NotificationManager {
} }
} }
const notificationManager = new NotificationManager(); const notifier = new Notifier();
export default notificationManager; export default notifier;

View File

@ -1,13 +1,13 @@
import db from "./db"; import db from "./db";
class Prefs { class Prefs {
async setSelectedSubscriptionId(selectedSubscriptionId) { async setSound(sound) {
db.prefs.put({key: 'selectedSubscriptionId', value: selectedSubscriptionId}); db.prefs.put({key: 'sound', value: sound.toString()});
} }
async selectedSubscriptionId() { async sound() {
const selectedSubscriptionId = await db.prefs.get('selectedSubscriptionId'); const sound = await db.prefs.get('sound');
return (selectedSubscriptionId) ? selectedSubscriptionId.value : ""; return (sound) ? sound.value : "mixkit-correct-answer-tone";
} }
async setMinPriority(minPriority) { async setMinPriority(minPriority) {

View File

@ -41,7 +41,7 @@ class SubscriptionManager {
if (exists) { if (exists) {
return false; 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, { await db.subscriptions.update(subscriptionId, {
last: notification.id last: notification.id
}); });

View File

@ -121,6 +121,11 @@ export const subscriptionRoute = (subscription) => {
return `/${subscription.topic}`; 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 // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
export async function* fetchLinesIterator(fileURL, headers) { export async function* fetchLinesIterator(fileURL, headers) {
const utf8Decoder = new TextDecoder('utf-8'); const utf8Decoder = new TextDecoder('utf-8');

View File

@ -9,7 +9,7 @@ import theme from "./theme";
import connectionManager from "../app/ConnectionManager"; import connectionManager from "../app/ConnectionManager";
import Navigation from "./Navigation"; import Navigation from "./Navigation";
import ActionBar from "./ActionBar"; import ActionBar from "./ActionBar";
import notificationManager from "../app/NotificationManager"; import notifier from "../app/Notifier";
import NoTopics from "./NoTopics"; import NoTopics from "./NoTopics";
import Preferences from "./Preferences"; import Preferences from "./Preferences";
import {useLiveQuery} from "dexie-react-hooks"; import {useLiveQuery} from "dexie-react-hooks";
@ -26,6 +26,11 @@ import {subscriptionRoute} from "../app/utils";
// TODO sound // TODO sound
// TODO "copy url" toast // TODO "copy url" toast
// TODO "copy link url" button // 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 = () => { const App = () => {
return ( return (
@ -40,7 +45,7 @@ const App = () => {
const Root = () => { const Root = () => {
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted()); const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted());
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const users = useLiveQuery(() => userManager.all()); const users = useLiveQuery(() => userManager.all());
@ -54,7 +59,7 @@ const Root = () => {
}; };
const handleRequestPermission = () => { const handleRequestPermission = () => {
notificationManager.maybeRequestPermission(granted => setNotificationsGranted(granted)); notifier.maybeRequestPermission(granted => setNotificationsGranted(granted));
}; };
useEffect(() => { useEffect(() => {
@ -68,7 +73,7 @@ const Root = () => {
const added = await subscriptionManager.addNotification(subscriptionId, notification); const added = await subscriptionManager.addNotification(subscriptionId, notification);
if (added) { if (added) {
const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); // FIXME const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); // FIXME
await notificationManager.notify(subscriptionId, notification, defaultClickAction) await notifier.notify(subscriptionId, notification, defaultClickAction)
} }
} catch (e) { } catch (e) {
console.error(`[App] Error handling notification`, e); console.error(`[App] Error handling notification`, e);

View File

@ -19,6 +19,7 @@ import {Paragraph} from "./styles";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import Container from "@mui/material/Container"; import Container from "@mui/material/Container";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
@ -31,6 +32,7 @@ import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent"; import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions"; import DialogActions from "@mui/material/DialogActions";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import {playSound} from "../app/utils";
const Preferences = () => { const Preferences = () => {
return ( return (
@ -50,6 +52,7 @@ const Notifications = () => {
Notifications Notifications
</Typography> </Typography>
<PrefGroup> <PrefGroup>
<Sound/>
<MinPriority/> <MinPriority/>
<DeleteAfter/> <DeleteAfter/>
</PrefGroup> </PrefGroup>
@ -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 (
<Pref title="Notification sound">
<div style={{ display: 'flex', width: '100%' }}>
<FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
<Select value={sound} onChange={handleChange}>
<MenuItem value={"none"}>No sound</MenuItem>
<MenuItem value={"mixkit-correct-answer-tone"}>Ding</MenuItem>
<MenuItem value={"juntos"}>Juntos</MenuItem>
<MenuItem value={"pristine"}>Pristine</MenuItem>
<MenuItem value={"mixkit-software-interface-start"}>Dadum</MenuItem>
<MenuItem value={"mixkit-message-pop-alert"}>Pop</MenuItem>
<MenuItem value={"mixkit-long-pop"}>Pop swoosh</MenuItem>
<MenuItem value={"beep"}>Beep</MenuItem>
</Select>
</FormControl>
<IconButton onClick={() => playSound(sound)} disabled={sound === "none"}>
<PlayArrowIcon />
</IconButton>
</div>
</Pref>
)
};
const MinPriority = () => { const MinPriority = () => {
const minPriority = useLiveQuery(() => prefs.minPriority()); const minPriority = useLiveQuery(async () => prefs.minPriority());
const handleChange = async (ev) => { const handleChange = async (ev) => {
await prefs.setMinPriority(ev.target.value); await prefs.setMinPriority(ev.target.value);
} }