Support sounds
This commit is contained in:
parent
09b128f27a
commit
dc7ca6e405
13 changed files with 73 additions and 16 deletions
BIN
web/public/static/sounds/beep.mp3
Normal file
BIN
web/public/static/sounds/beep.mp3
Normal file
Binary file not shown.
BIN
web/public/static/sounds/juntos.mp3
Normal file
BIN
web/public/static/sounds/juntos.mp3
Normal file
Binary file not shown.
BIN
web/public/static/sounds/mixkit-correct-answer-tone.mp3
Normal file
BIN
web/public/static/sounds/mixkit-correct-answer-tone.mp3
Normal file
Binary file not shown.
BIN
web/public/static/sounds/mixkit-long-pop.mp3
Normal file
BIN
web/public/static/sounds/mixkit-long-pop.mp3
Normal file
Binary file not shown.
BIN
web/public/static/sounds/mixkit-message-pop-alert.mp3
Normal file
BIN
web/public/static/sounds/mixkit-message-pop-alert.mp3
Normal file
Binary file not shown.
BIN
web/public/static/sounds/mixkit-software-interface-start.mp3
Normal file
BIN
web/public/static/sounds/mixkit-software-interface-start.mp3
Normal file
Binary file not shown.
BIN
web/public/static/sounds/pristine.mp3
Normal file
BIN
web/public/static/sounds/pristine.mp3
Normal file
Binary file not shown.
|
@ -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;
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue