Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
import type { ICustomSound } from '@rocket.chat/core-typings';
|
|
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
|
import { CustomSoundContext, useEndpoint, useStream, useUserPreference } from '@rocket.chat/ui-contexts';
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { useEffect, useMemo, useRef, type ReactNode } from 'react';
|
|
|
|
import { defaultSounds, getCustomSoundURL, formatVolume } from './lib';
|
|
import { useUserSoundPreferences } from '../../hooks/useUserSoundPreferences';
|
|
|
|
export type CustomSoundProviderProps = {
|
|
children?: ReactNode;
|
|
};
|
|
|
|
const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => {
|
|
const audioRefs = useRef<HTMLAudioElement[]>([]);
|
|
|
|
const queryClient = useQueryClient();
|
|
const streamAll = useStream('notify-all');
|
|
const getCustomSounds = useEndpoint('GET', '/v1/custom-sounds.list');
|
|
|
|
const newRoomNotification = useUserPreference<string>('newRoomNotification') || 'door';
|
|
const newMessageNotification = useUserPreference<string>('newMessageNotification') || 'chime';
|
|
const { notificationsSoundVolume, voipRingerVolume } = useUserSoundPreferences();
|
|
|
|
const { data: list } = useQuery({
|
|
queryFn: async (): Promise<Omit<ICustomSound, '_updatedAt'>[]> => {
|
|
// `/v1/custom-sounds.list` is paginated, so we page through all results to
|
|
// load every custom sound into the provider (the legacy `listCustomSounds`
|
|
// method returned them all at once).
|
|
const sounds: Awaited<ReturnType<typeof getCustomSounds>>['sounds'] = [];
|
|
let total = Infinity;
|
|
while (sounds.length < total) {
|
|
const page = await getCustomSounds({ count: 100, offset: sounds.length });
|
|
total = page.total;
|
|
sounds.push(...page.sounds);
|
|
if (!page.sounds.length) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!sounds.length) {
|
|
return defaultSounds;
|
|
}
|
|
return [...sounds.map(({ _updatedAt: _, ...sound }) => ({ ...sound, src: getCustomSoundURL(sound) })), ...defaultSounds];
|
|
},
|
|
queryKey: ['listCustomSounds'],
|
|
initialData: defaultSounds,
|
|
});
|
|
|
|
const play = useStableCallback((soundId: ICustomSound['_id'], { volume = 1, loop = false } = {}) => {
|
|
stop(soundId);
|
|
|
|
const item = list?.find(({ _id }) => _id === soundId);
|
|
if (!item?.src) {
|
|
console.error('Unable to play sound', soundId);
|
|
return;
|
|
}
|
|
|
|
const audio = new Audio(item.src);
|
|
audio.volume = volume;
|
|
audio.loop = loop;
|
|
audio.id = soundId;
|
|
audio.play();
|
|
|
|
audioRefs.current = [...audioRefs.current, audio];
|
|
|
|
return () => {
|
|
stop(soundId);
|
|
};
|
|
});
|
|
|
|
const pause = useStableCallback((soundId: ICustomSound['_id']) => {
|
|
const current = audioRefs.current?.find(({ id }) => id === soundId);
|
|
if (current) {
|
|
current.pause();
|
|
audioRefs.current = audioRefs.current.filter(({ id }) => id !== soundId);
|
|
}
|
|
});
|
|
|
|
const stop = useStableCallback((soundId: ICustomSound['_id']) => {
|
|
const current = audioRefs.current?.find(({ id }) => id === soundId);
|
|
if (current) {
|
|
current.load();
|
|
audioRefs.current = audioRefs.current.filter(({ id }) => id !== soundId);
|
|
}
|
|
});
|
|
|
|
const contextValue = useMemo(() => {
|
|
const notificationSounds = {
|
|
playNewRoom: () => play(newRoomNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }),
|
|
playNewRoomLoop: () => play(newRoomNotification, { loop: true, volume: formatVolume(notificationsSoundVolume) }),
|
|
playNewMessage: () => play(newMessageNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }),
|
|
playNewMessageCustom: (soundId: ICustomSound['_id']) =>
|
|
play(soundId, { loop: false, volume: formatVolume(notificationsSoundVolume) }),
|
|
stopNewRoom: () => stop(newRoomNotification),
|
|
stopNewMessage: () => stop(newMessageNotification),
|
|
};
|
|
const voipSounds = {
|
|
playRinger: () => play('telephone', { loop: true, volume: formatVolume(voipRingerVolume) }),
|
|
playDialer: () => play('outbound-call-ringing', { loop: true, volume: formatVolume(voipRingerVolume) }),
|
|
playCallEnded: () => play('call-ended', { loop: false, volume: formatVolume(voipRingerVolume) }),
|
|
stopRinger: () => stop('telephone'),
|
|
stopDialer: () => stop('outbound-call-ringing'),
|
|
stopCallEnded: () => stop('call-ended'),
|
|
stopAll: () => {
|
|
stop('telephone');
|
|
stop('outbound-call-ringing');
|
|
stop('call-ended');
|
|
},
|
|
};
|
|
const callSounds = {
|
|
playRinger: () => play('ringtone', { loop: true, volume: formatVolume(voipRingerVolume) }),
|
|
playDialer: () => play('dialtone', { loop: true, volume: formatVolume(voipRingerVolume) }),
|
|
stopRinger: () => stop('ringtone'),
|
|
stopDialer: () => stop('dialtone'),
|
|
};
|
|
return {
|
|
list,
|
|
notificationSounds,
|
|
callSounds,
|
|
voipSounds,
|
|
play,
|
|
pause,
|
|
stop,
|
|
};
|
|
}, [list, newMessageNotification, newRoomNotification, notificationsSoundVolume, pause, play, stop, voipRingerVolume]);
|
|
|
|
useEffect(() => {
|
|
return streamAll('public-info', ([key]) => {
|
|
switch (key) {
|
|
case 'updateCustomSound':
|
|
queryClient.invalidateQueries({ queryKey: ['listCustomSounds'] });
|
|
break;
|
|
case 'deleteCustomSound':
|
|
queryClient.invalidateQueries({ queryKey: ['listCustomSounds'] });
|
|
|
|
break;
|
|
}
|
|
});
|
|
}, [queryClient, streamAll]);
|
|
|
|
return <CustomSoundContext.Provider value={contextValue}>{children}</CustomSoundContext.Provider>;
|
|
};
|
|
|
|
export default CustomSoundProvider;
|