feat(mobx): migrate audio settings

This commit is contained in:
Paul Makles 2021-12-16 22:05:31 +00:00
parent c7df0088fc
commit 120e6a35d8
13 changed files with 147 additions and 188 deletions

View file

@ -1,29 +0,0 @@
import call_join from "./call_join.mp3";
import call_leave from "./call_leave.mp3";
import message from "./message.mp3";
import outbound from "./outbound.mp3";
const SoundMap: { [key in Sounds]: string } = {
message,
outbound,
call_join,
call_leave,
};
export type Sounds = "message" | "outbound" | "call_join" | "call_leave";
export const SOUNDS_ARRAY: Sounds[] = [
"message",
"outbound",
"call_join",
"call_leave",
];
export function playSound(sound: Sounds) {
const file = SoundMap[sound];
const el = new Audio(file);
try {
el.play();
} catch (err) {
console.error("Failed to play audio file", file, err);
}
}

View file

@ -1,9 +1,9 @@
import { EmojiPacks } from "../../redux/reducers/settings"; export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji";
let EMOJI_PACK = "mutant"; let EMOJI_PACK: EmojiPack = "mutant";
const REVISION = 3; const REVISION = 3;
export function setGlobalEmojiPack(pack: EmojiPacks) { export function setGlobalEmojiPack(pack: EmojiPack) {
EMOJI_PACK = pack; EMOJI_PACK = pack;
} }

View file

@ -21,10 +21,8 @@ import {
} from "../../../lib/renderer/Singleton"; } from "../../../lib/renderer/Singleton";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
import { dispatch, getState } from "../../../redux";
import { Reply } from "../../../redux/reducers/queue"; import { Reply } from "../../../redux/reducers/queue";
import { SoundContext } from "../../../context/Settings";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { import {
FileUploader, FileUploader,
@ -123,7 +121,6 @@ export default observer(({ channel }: Props) => {
}); });
const [typing, setTyping] = useState<boolean | number>(false); const [typing, setTyping] = useState<boolean | number>(false);
const [replies, setReplies] = useState<Reply[]>([]); const [replies, setReplies] = useState<Reply[]>([]);
const playSound = useContext(SoundContext);
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useContext(AppContext); const client = useContext(AppContext);
const translate = useTranslation(); const translate = useTranslation();
@ -242,7 +239,7 @@ export default observer(({ channel }: Props) => {
} }
} }
} else { } else {
playSound("outbound"); state.settings.sounds.playSound("outbound");
state.queue.add(nonce, channel._id, { state.queue.add(nonce, channel._id, {
_id: nonce, _id: nonce,
@ -351,7 +348,7 @@ export default observer(({ channel }: Props) => {
setMessage(); setMessage();
setReplies([]); setReplies([]);
playSound("outbound"); state.settings.sounds.playSound("outbound");
if (files.length > CAN_UPLOAD_AT_ONCE) { if (files.length > CAN_UPLOAD_AT_ONCE) {
setUploadState({ setUploadState({

View file

@ -2,8 +2,7 @@ import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { EmojiPack } from "../../../mobx/stores/Settings"; import { EmojiPack } from "../../common/Emoji";
import mutantSVG from "./mutant_emoji.svg"; import mutantSVG from "./mutant_emoji.svg";
import notoSVG from "./noto_emoji.svg"; import notoSVG from "./noto_emoji.svg";
import openmojiSVG from "./openmoji_emoji.svg"; import openmojiSVG from "./openmoji_emoji.svg";

View file

@ -1,61 +0,0 @@
// This code is more or less redundant, but settings has so little state
// updates that I can't be asked to pass everything through props each
// time when I can just use the Context API.
//
// Replace references to SettingsContext with connectState in the future
// if it does cause problems though.
//
// This now also supports Audio stuff.
import defaultsDeep from "lodash.defaultsdeep";
import { createContext } from "preact";
import { useMemo } from "preact/hooks";
import { connectState } from "../redux/connector";
import {
DEFAULT_SOUNDS,
Settings,
SoundOptions,
} from "../redux/reducers/settings";
import { playSound, Sounds } from "../assets/sounds/Audio";
import { Children } from "../types/Preact";
export const SettingsContext = createContext<Settings>({});
export const SoundContext = createContext<(sound: Sounds) => void>(null!);
interface Props {
children?: Children;
settings: Settings;
}
function SettingsProvider({ settings, children }: Props) {
const play = useMemo(() => {
const enabled: SoundOptions = defaultsDeep(
settings.notification?.sounds ?? {},
DEFAULT_SOUNDS,
);
return (sound: Sounds) => {
if (enabled[sound]) {
playSound(sound);
}
};
}, [settings.notification]);
return (
<SettingsContext.Provider value={settings}>
<SoundContext.Provider value={play}>
{children}
</SoundContext.Provider>
</SettingsContext.Provider>
);
}
export default connectState<Omit<Props, "settings">>(
SettingsProvider,
(state) => {
return {
settings: state.settings,
};
},
);

View file

@ -4,7 +4,6 @@ import State from "../redux/State";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
import Locale from "./Locale"; import Locale from "./Locale";
import Settings from "./Settings";
import Theme from "./Theme"; import Theme from "./Theme";
import Intermediate from "./intermediate/Intermediate"; import Intermediate from "./intermediate/Intermediate";
import Client from "./revoltjs/RevoltClient"; import Client from "./revoltjs/RevoltClient";
@ -17,13 +16,11 @@ export default function Context({ children }: { children: Children }) {
return ( return (
<Router basename={import.meta.env.BASE_URL}> <Router basename={import.meta.env.BASE_URL}>
<State> <State>
<Settings> <Locale>
<Locale> <Intermediate>
<Intermediate> <Client>{children}</Client>
<Client>{children}</Client> </Intermediate>
</Intermediate> </Locale>
</Locale>
</Settings>
<Theme /> <Theme />
</State> </State>
</Router> </Router>

View file

@ -9,21 +9,9 @@ import { useCallback, useContext, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n"; import { useTranslation } from "../../lib/i18n";
import { useApplicationState } from "../../mobx/State"; import { useApplicationState } from "../../mobx/State";
import { connectState } from "../../redux/connector";
import {
getNotificationState,
Notifications,
shouldNotify,
} from "../../redux/reducers/notifications";
import { NotificationOptions } from "../../redux/reducers/settings";
import { SoundContext } from "../Settings";
import { AppContext } from "./RevoltClient"; import { AppContext } from "./RevoltClient";
interface Props {
options?: NotificationOptions;
}
const notifications: { [key: string]: Notification } = {}; const notifications: { [key: string]: Notification } = {};
async function createNotification( async function createNotification(
@ -38,10 +26,11 @@ async function createNotification(
} }
} }
function Notifier({ options }: Props) { function Notifier() {
const translate = useTranslation(); const translate = useTranslation();
const showNotification = options?.desktopEnabled ?? false; const state = useApplicationState();
const notifs = useApplicationState().notifications; const notifs = state.notifications;
const showNotification = state.settings.get("notifications:desktop");
const client = useContext(AppContext); const client = useContext(AppContext);
const { guild: guild_id, channel: channel_id } = useParams<{ const { guild: guild_id, channel: channel_id } = useParams<{
@ -49,14 +38,13 @@ function Notifier({ options }: Props) {
channel: string; channel: string;
}>(); }>();
const history = useHistory(); const history = useHistory();
const playSound = useContext(SoundContext);
const message = useCallback( const message = useCallback(
async (msg: Message) => { async (msg: Message) => {
if (msg.channel_id === channel_id && document.hasFocus()) return; if (msg.channel_id === channel_id && document.hasFocus()) return;
if (!notifs.shouldNotify(msg)) return; if (!notifs.shouldNotify(msg)) return;
playSound("message"); state.settings.sounds.playSound("message");
if (!showNotification) return; if (!showNotification) return;
let title; let title;
@ -209,7 +197,7 @@ function Notifier({ options }: Props) {
channel_id, channel_id,
client, client,
notifs, notifs,
playSound, state,
], ],
); );
@ -257,7 +245,7 @@ function Notifier({ options }: Props) {
}; };
}, [ }, [
client, client,
playSound, state,
guild_id, guild_id,
channel_id, channel_id,
showNotification, showNotification,
@ -285,27 +273,17 @@ function Notifier({ options }: Props) {
return null; return null;
} }
const NotifierComponent = connectState(
Notifier,
(state) => {
return {
options: state.settings.notification,
};
},
true,
);
export default function NotificationsComponent() { export default function NotificationsComponent() {
return ( return (
<Switch> <Switch>
<Route path="/server/:server/channel/:channel"> <Route path="/server/:server/channel/:channel">
<NotifierComponent /> <Notifier />
</Route> </Route>
<Route path="/channel/:channel"> <Route path="/channel/:channel">
<NotifierComponent /> <Notifier />
</Route> </Route>
<Route path="/"> <Route path="/">
<NotifierComponent /> <Notifier />
</Route> </Route>
</Switch> </Switch>
); );

View file

@ -4,17 +4,13 @@ import { mapToRecord } from "../../lib/conversion";
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme"; import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
import { Sounds } from "../../assets/sounds/Audio"; import { EmojiPack } from "../../components/common/Emoji";
import Persistent from "../interfaces/Persistent"; import Persistent from "../interfaces/Persistent";
import Store from "../interfaces/Store"; import Store from "../interfaces/Store";
import SAudio, { SoundOptions } from "./helpers/SAudio";
import STheme from "./helpers/STheme"; import STheme from "./helpers/STheme";
export type SoundOptions = {
[key in Sounds]?: boolean;
};
export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji";
interface ISettings { interface ISettings {
"notifications:desktop": boolean; "notifications:desktop": boolean;
"notifications:sounds": SoundOptions; "notifications:sounds": SoundOptions;
@ -37,6 +33,7 @@ export default class Settings implements Store, Persistent<ISettings> {
private data: ObservableMap<string, unknown>; private data: ObservableMap<string, unknown>;
theme: STheme; theme: STheme;
sounds: SAudio;
/** /**
* Construct new Settings store. * Construct new Settings store.
@ -46,6 +43,7 @@ export default class Settings implements Store, Persistent<ISettings> {
makeAutoObservable(this); makeAutoObservable(this);
this.theme = new STheme(this); this.theme = new STheme(this);
this.sounds = new SAudio(this);
} }
get id() { get id() {

View file

@ -0,0 +1,107 @@
import { makeAutoObservable, computed, action } from "mobx";
import Settings from "../Settings";
import call_join from "./call_join.mp3";
import call_leave from "./call_leave.mp3";
import message from "./message.mp3";
import outbound from "./outbound.mp3";
export type Sounds = "message" | "outbound" | "call_join" | "call_leave";
export interface Sound {
enabled: boolean;
path: string;
}
export type SoundOptions = {
[key in Sounds]?: Partial<Sound>;
};
export const DefaultSoundPack: { [key in Sounds]: string } = {
message,
outbound,
call_join,
call_leave,
};
export const ALL_SOUNDS: Sounds[] = [
"message",
"outbound",
"call_join",
"call_leave",
];
export const DEFAULT_SOUNDS: Sounds[] = ["message", "call_join", "call_leave"];
/**
* Helper class for reading and writing themes.
*/
export default class SAudio {
private settings: Settings;
private cache: Map<string, HTMLAudioElement>;
/**
* Construct a new sound helper.
* @param settings Settings parent class
*/
constructor(settings: Settings) {
this.settings = settings;
makeAutoObservable(this);
this.cache = new Map();
// Asynchronously load Audio files into cache.
setTimeout(() => this.loadCache(), 0);
}
@action setEnabled(sound: Sounds, enabled: boolean) {
const obj = this.settings.get("notifications:sounds");
this.settings.set("notifications:sounds", {
...obj,
[sound]: {
...obj?.[sound],
enabled,
},
});
}
@computed getSound(sound: Sounds, options?: SoundOptions): Sound {
return {
path: DefaultSoundPack[sound],
enabled: DEFAULT_SOUNDS.includes(sound),
...(options ?? this.settings.get("notifications:sounds"))?.[sound],
};
}
@computed getState(): ({ id: Sounds } & Sound)[] {
const options = this.settings.get("notifications:sounds");
return ALL_SOUNDS.map((id) => {
return { id, ...this.getSound(id, options) };
});
}
getAudio(path: string) {
if (this.cache.has(path)) {
return this.cache.get(path)!;
} else {
const el = new Audio(path);
this.cache.set(path, el);
return el;
}
}
loadCache() {
this.getState().map(({ path }) => this.getAudio(path));
}
playSound(sound: Sounds) {
const definition = this.getSound(sound);
if (definition.enabled) {
const audio = this.getAudio(definition.path);
try {
audio.play();
} catch (err) {
console.error("Hit error while playing", sound + ":", err);
}
}
}
}

View file

@ -1,26 +1,19 @@
import defaultsDeep from "lodash.defaultsdeep";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { urlBase64ToUint8Array } from "../../../lib/conversion"; import { urlBase64ToUint8Array } from "../../../lib/conversion";
import { useApplicationState } from "../../../mobx/State";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector"; import { connectState } from "../../../redux/connector";
import { import { NotificationOptions } from "../../../redux/reducers/settings";
DEFAULT_SOUNDS,
NotificationOptions,
SoundOptions,
} from "../../../redux/reducers/settings";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Checkbox from "../../../components/ui/Checkbox"; import Checkbox from "../../../components/ui/Checkbox";
import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
interface Props { interface Props {
options?: NotificationOptions; options?: NotificationOptions;
} }
@ -28,6 +21,7 @@ interface Props {
export function Component({ options }: Props) { export function Component({ options }: Props) {
const client = useContext(AppContext); const client = useContext(AppContext);
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const sounds = useApplicationState().settings.sounds;
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>( const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
undefined, undefined,
); );
@ -42,10 +36,6 @@ export function Component({ options }: Props) {
}); });
}, []); }, []);
const enabledSounds: SoundOptions = defaultsDeep(
options?.sounds ?? {},
DEFAULT_SOUNDS,
);
return ( return (
<div className={styles.notifications}> <div className={styles.notifications}>
<h3> <h3>
@ -125,24 +115,12 @@ export function Component({ options }: Props) {
<h3> <h3>
<Text id="app.settings.pages.notifications.sounds" /> <Text id="app.settings.pages.notifications.sounds" />
</h3> </h3>
{SOUNDS_ARRAY.map((key) => ( {sounds.getState().map(({ id, enabled }) => (
<Checkbox <Checkbox
key={key} key={id}
checked={!!enabledSounds[key]} checked={enabled}
onChange={(enabled) => onChange={(enabled) => sounds.setEnabled(id, enabled)}>
dispatch({ <Text id={`app.settings.pages.notifications.sound.${id}`} />
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
options: {
sounds: {
...options?.sounds,
[key]: enabled,
},
},
})
}>
<Text
id={`app.settings.pages.notifications.sound.${key}`}
/>
</Checkbox> </Checkbox>
))} ))}
</div> </div>

View file

@ -2,9 +2,10 @@ import type { Theme, ThemeOptions } from "../../context/Theme";
import { setGlobalEmojiPack } from "../../components/common/Emoji"; import { setGlobalEmojiPack } from "../../components/common/Emoji";
import type { Sounds } from "../../assets/sounds/Audio";
import type { SyncUpdateAction } from "./sync"; import type { SyncUpdateAction } from "./sync";
type Sounds = "message" | "outbound" | "call_join" | "call_leave";
export type SoundOptions = { export type SoundOptions = {
[key in Sounds]?: boolean; [key in Sounds]?: boolean;
}; };

View file

@ -78,11 +78,10 @@ export function UI() {
render( render(
<> <>
<Theme> <UIDemo>
<UIDemo> <UI />
<UI /> </UIDemo>
</UIDemo> <Theme />
</Theme>
</>, </>,
document.getElementById("app")!, document.getElementById("app")!,
); );

View file

@ -3877,11 +3877,6 @@ serialize-javascript@^4.0.0:
dependencies: dependencies:
randombytes "^2.1.0" randombytes "^2.1.0"
shade-blend-color@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shade-blend-color/-/shade-blend-color-1.0.0.tgz#cfa10d3673a22ba31d552a0e793b708bc24be0bc"
integrity sha512-Tnp/ppF5h3YhPCpeHiZJ2DRnvmo4luu9qpMhuksCT+QInIXJ9alA3Vd9klfEi+RY8Oh7MaK5vzH/qcLo892L1g==
shallowequal@^1.1.0: shallowequal@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"