snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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 1s
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 1s
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './CustomSoundProvider';
|
||||
@@ -0,0 +1,19 @@
|
||||
import { formatVolume } from './formatVolume';
|
||||
|
||||
describe('formatVolume', () => {
|
||||
it('returns 1 if volume is 100', () => {
|
||||
expect(formatVolume(100)).toBe(1);
|
||||
});
|
||||
|
||||
it('returns 1 if volume is 200', () => {
|
||||
expect(formatVolume(200)).toBe(1);
|
||||
});
|
||||
|
||||
it('returns 0.5 if volume is 50', () => {
|
||||
expect(formatVolume(50)).toBe(0.5);
|
||||
});
|
||||
|
||||
it('returns 0 if volume is -10', () => {
|
||||
expect(formatVolume(-10)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export const formatVolume = (volume: number) => {
|
||||
const clamped = Math.max(0, Math.min(volume, 100));
|
||||
return Number((clamped / 100).toPrecision(2));
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ICustomSound } from '@rocket.chat/core-typings';
|
||||
|
||||
import { getURL } from '../../../lib/getURL';
|
||||
|
||||
export const getAssetUrl = (asset: string, params?: Record<string, any>) => getURL(asset, params, undefined, true);
|
||||
|
||||
export const getCustomSoundURL = (sound: Pick<ICustomSound, '_id' | 'extension'> & Pick<Partial<ICustomSound>, 'random'>) => {
|
||||
return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 });
|
||||
};
|
||||
|
||||
// Translation keys for sound names
|
||||
export const soundTranslationKeys: Record<string, string> = {
|
||||
'chime': 'Sound_Chime',
|
||||
'door': 'Sound_Door',
|
||||
'beep': 'Sound_Beep',
|
||||
'chelle': 'Sound_Chelle',
|
||||
'ding': 'Sound_Ding',
|
||||
'droplet': 'Sound_Droplet',
|
||||
'highbell': 'Sound_Highbell',
|
||||
'seasons': 'Sound_Seasons',
|
||||
'telephone': 'Sound_Telephone',
|
||||
'outbound-call-ringing': 'Sound_Outbound_Call_Ringing',
|
||||
'call-ended': 'Sound_Call_Ended',
|
||||
'dialtone': 'Sound_Dialtone',
|
||||
'ringtone': 'Sound_Ringtone',
|
||||
};
|
||||
|
||||
export const defaultSounds: Omit<ICustomSound, '_updatedAt'>[] = [
|
||||
{ _id: 'chime', name: 'Sound_Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') },
|
||||
{ _id: 'door', name: 'Sound_Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') },
|
||||
{ _id: 'beep', name: 'Sound_Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') },
|
||||
{ _id: 'chelle', name: 'Sound_Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') },
|
||||
{ _id: 'ding', name: 'Sound_Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') },
|
||||
{ _id: 'droplet', name: 'Sound_Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') },
|
||||
{ _id: 'highbell', name: 'Sound_Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') },
|
||||
{ _id: 'seasons', name: 'Sound_Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') },
|
||||
{ _id: 'telephone', name: 'Sound_Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') },
|
||||
{
|
||||
_id: 'outbound-call-ringing',
|
||||
name: 'Sound_Outbound_Call_Ringing',
|
||||
extension: 'mp3',
|
||||
src: getAssetUrl('sounds/outbound-call-ringing.mp3'),
|
||||
},
|
||||
{ _id: 'call-ended', name: 'Sound_Call_Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') },
|
||||
{ _id: 'dialtone', name: 'Sound_Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') },
|
||||
{ _id: 'ringtone', name: 'Sound_Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') },
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './helpers';
|
||||
export * from './formatVolume';
|
||||
Reference in New Issue
Block a user