From 120e6a35d81fca32515c0da2bb665573f1f7b000 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Thu, 16 Dec 2021 22:05:31 +0000 Subject: [PATCH] feat(mobx): migrate audio settings --- src/assets/sounds/Audio.ts | 29 ----- src/components/common/Emoji.tsx | 6 +- .../common/messaging/MessageBox.tsx | 7 +- .../settings/appearance/EmojiSelector.tsx | 3 +- src/context/Settings.tsx | 61 ---------- src/context/index.tsx | 13 +-- src/context/revoltjs/Notifications.tsx | 42 ++----- src/mobx/stores/Settings.ts | 12 +- src/mobx/stores/helpers/SAudio.ts | 107 ++++++++++++++++++ src/pages/settings/panes/Notifications.tsx | 38 ++----- src/redux/reducers/settings.ts | 3 +- ui/ui.tsx | 9 +- yarn.lock | 5 - 13 files changed, 147 insertions(+), 188 deletions(-) delete mode 100644 src/assets/sounds/Audio.ts delete mode 100644 src/context/Settings.tsx create mode 100644 src/mobx/stores/helpers/SAudio.ts diff --git a/src/assets/sounds/Audio.ts b/src/assets/sounds/Audio.ts deleted file mode 100644 index 5d2cf91a..00000000 --- a/src/assets/sounds/Audio.ts +++ /dev/null @@ -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); - } -} diff --git a/src/components/common/Emoji.tsx b/src/components/common/Emoji.tsx index 361df955..31d11df8 100644 --- a/src/components/common/Emoji.tsx +++ b/src/components/common/Emoji.tsx @@ -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; -export function setGlobalEmojiPack(pack: EmojiPacks) { +export function setGlobalEmojiPack(pack: EmojiPack) { EMOJI_PACK = pack; } diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index 91b823a2..513e6280 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -21,10 +21,8 @@ import { } from "../../../lib/renderer/Singleton"; import { useApplicationState } from "../../../mobx/State"; -import { dispatch, getState } from "../../../redux"; import { Reply } from "../../../redux/reducers/queue"; -import { SoundContext } from "../../../context/Settings"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { FileUploader, @@ -123,7 +121,6 @@ export default observer(({ channel }: Props) => { }); const [typing, setTyping] = useState(false); const [replies, setReplies] = useState([]); - const playSound = useContext(SoundContext); const { openScreen } = useIntermediate(); const client = useContext(AppContext); const translate = useTranslation(); @@ -242,7 +239,7 @@ export default observer(({ channel }: Props) => { } } } else { - playSound("outbound"); + state.settings.sounds.playSound("outbound"); state.queue.add(nonce, channel._id, { _id: nonce, @@ -351,7 +348,7 @@ export default observer(({ channel }: Props) => { setMessage(); setReplies([]); - playSound("outbound"); + state.settings.sounds.playSound("outbound"); if (files.length > CAN_UPLOAD_AT_ONCE) { setUploadState({ diff --git a/src/components/settings/appearance/EmojiSelector.tsx b/src/components/settings/appearance/EmojiSelector.tsx index 84232664..ac6d623c 100644 --- a/src/components/settings/appearance/EmojiSelector.tsx +++ b/src/components/settings/appearance/EmojiSelector.tsx @@ -2,8 +2,7 @@ import styled from "styled-components"; import { Text } from "preact-i18n"; -import { EmojiPack } from "../../../mobx/stores/Settings"; - +import { EmojiPack } from "../../common/Emoji"; import mutantSVG from "./mutant_emoji.svg"; import notoSVG from "./noto_emoji.svg"; import openmojiSVG from "./openmoji_emoji.svg"; diff --git a/src/context/Settings.tsx b/src/context/Settings.tsx deleted file mode 100644 index 78e268cc..00000000 --- a/src/context/Settings.tsx +++ /dev/null @@ -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({}); -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 ( - - - {children} - - - ); -} - -export default connectState>( - SettingsProvider, - (state) => { - return { - settings: state.settings, - }; - }, -); diff --git a/src/context/index.tsx b/src/context/index.tsx index 4e6ec63a..1b6ee4ec 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -4,7 +4,6 @@ import State from "../redux/State"; import { Children } from "../types/Preact"; import Locale from "./Locale"; -import Settings from "./Settings"; import Theme from "./Theme"; import Intermediate from "./intermediate/Intermediate"; import Client from "./revoltjs/RevoltClient"; @@ -17,13 +16,11 @@ export default function Context({ children }: { children: Children }) { return ( - - - - {children} - - - + + + {children} + + diff --git a/src/context/revoltjs/Notifications.tsx b/src/context/revoltjs/Notifications.tsx index 04d5c5f9..37707032 100644 --- a/src/context/revoltjs/Notifications.tsx +++ b/src/context/revoltjs/Notifications.tsx @@ -9,21 +9,9 @@ import { useCallback, useContext, useEffect } from "preact/hooks"; import { useTranslation } from "../../lib/i18n"; 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"; -interface Props { - options?: NotificationOptions; -} - const notifications: { [key: string]: Notification } = {}; async function createNotification( @@ -38,10 +26,11 @@ async function createNotification( } } -function Notifier({ options }: Props) { +function Notifier() { const translate = useTranslation(); - const showNotification = options?.desktopEnabled ?? false; - const notifs = useApplicationState().notifications; + const state = useApplicationState(); + const notifs = state.notifications; + const showNotification = state.settings.get("notifications:desktop"); const client = useContext(AppContext); const { guild: guild_id, channel: channel_id } = useParams<{ @@ -49,14 +38,13 @@ function Notifier({ options }: Props) { channel: string; }>(); const history = useHistory(); - const playSound = useContext(SoundContext); const message = useCallback( async (msg: Message) => { if (msg.channel_id === channel_id && document.hasFocus()) return; if (!notifs.shouldNotify(msg)) return; - playSound("message"); + state.settings.sounds.playSound("message"); if (!showNotification) return; let title; @@ -209,7 +197,7 @@ function Notifier({ options }: Props) { channel_id, client, notifs, - playSound, + state, ], ); @@ -257,7 +245,7 @@ function Notifier({ options }: Props) { }; }, [ client, - playSound, + state, guild_id, channel_id, showNotification, @@ -285,27 +273,17 @@ function Notifier({ options }: Props) { return null; } -const NotifierComponent = connectState( - Notifier, - (state) => { - return { - options: state.settings.notification, - }; - }, - true, -); - export default function NotificationsComponent() { return ( - + - + - + ); diff --git a/src/mobx/stores/Settings.ts b/src/mobx/stores/Settings.ts index 4e35dfd7..9701cb12 100644 --- a/src/mobx/stores/Settings.ts +++ b/src/mobx/stores/Settings.ts @@ -4,17 +4,13 @@ import { mapToRecord } from "../../lib/conversion"; 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 Store from "../interfaces/Store"; +import SAudio, { SoundOptions } from "./helpers/SAudio"; import STheme from "./helpers/STheme"; -export type SoundOptions = { - [key in Sounds]?: boolean; -}; - -export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji"; - interface ISettings { "notifications:desktop": boolean; "notifications:sounds": SoundOptions; @@ -37,6 +33,7 @@ export default class Settings implements Store, Persistent { private data: ObservableMap; theme: STheme; + sounds: SAudio; /** * Construct new Settings store. @@ -46,6 +43,7 @@ export default class Settings implements Store, Persistent { makeAutoObservable(this); this.theme = new STheme(this); + this.sounds = new SAudio(this); } get id() { diff --git a/src/mobx/stores/helpers/SAudio.ts b/src/mobx/stores/helpers/SAudio.ts new file mode 100644 index 00000000..517d1212 --- /dev/null +++ b/src/mobx/stores/helpers/SAudio.ts @@ -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; +}; + +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; + + /** + * 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); + } + } + } +} diff --git a/src/pages/settings/panes/Notifications.tsx b/src/pages/settings/panes/Notifications.tsx index e1fc88f2..879f7c62 100644 --- a/src/pages/settings/panes/Notifications.tsx +++ b/src/pages/settings/panes/Notifications.tsx @@ -1,26 +1,19 @@ -import defaultsDeep from "lodash.defaultsdeep"; - import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useContext, useEffect, useState } from "preact/hooks"; import { urlBase64ToUint8Array } from "../../../lib/conversion"; +import { useApplicationState } from "../../../mobx/State"; import { dispatch } from "../../../redux"; import { connectState } from "../../../redux/connector"; -import { - DEFAULT_SOUNDS, - NotificationOptions, - SoundOptions, -} from "../../../redux/reducers/settings"; +import { NotificationOptions } from "../../../redux/reducers/settings"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; import Checkbox from "../../../components/ui/Checkbox"; -import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio"; - interface Props { options?: NotificationOptions; } @@ -28,6 +21,7 @@ interface Props { export function Component({ options }: Props) { const client = useContext(AppContext); const { openScreen } = useIntermediate(); + const sounds = useApplicationState().settings.sounds; const [pushEnabled, setPushEnabled] = useState( undefined, ); @@ -42,10 +36,6 @@ export function Component({ options }: Props) { }); }, []); - const enabledSounds: SoundOptions = defaultsDeep( - options?.sounds ?? {}, - DEFAULT_SOUNDS, - ); return (

@@ -125,24 +115,12 @@ export function Component({ options }: Props) {

- {SOUNDS_ARRAY.map((key) => ( + {sounds.getState().map(({ id, enabled }) => ( - dispatch({ - type: "SETTINGS_SET_NOTIFICATION_OPTIONS", - options: { - sounds: { - ...options?.sounds, - [key]: enabled, - }, - }, - }) - }> - + key={id} + checked={enabled} + onChange={(enabled) => sounds.setEnabled(id, enabled)}> + ))}
diff --git a/src/redux/reducers/settings.ts b/src/redux/reducers/settings.ts index 079aebdc..42248faa 100644 --- a/src/redux/reducers/settings.ts +++ b/src/redux/reducers/settings.ts @@ -2,9 +2,10 @@ import type { Theme, ThemeOptions } from "../../context/Theme"; import { setGlobalEmojiPack } from "../../components/common/Emoji"; -import type { Sounds } from "../../assets/sounds/Audio"; import type { SyncUpdateAction } from "./sync"; +type Sounds = "message" | "outbound" | "call_join" | "call_leave"; + export type SoundOptions = { [key in Sounds]?: boolean; }; diff --git a/ui/ui.tsx b/ui/ui.tsx index 46123b3a..5c4d9826 100644 --- a/ui/ui.tsx +++ b/ui/ui.tsx @@ -78,11 +78,10 @@ export function UI() { render( <> - - - - - + + + + , document.getElementById("app")!, ); diff --git a/yarn.lock b/yarn.lock index a538d37b..8e8c79f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3877,11 +3877,6 @@ serialize-javascript@^4.0.0: dependencies: 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: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"