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

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -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;

View File

@@ -0,0 +1 @@
export { default } from './CustomSoundProvider';

View File

@@ -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);
});
});

View File

@@ -0,0 +1,4 @@
export const formatVolume = (volume: number) => {
const clamped = Math.max(0, Math.min(volume, 100));
return Number((clamped / 100).toPrecision(2));
};

View File

@@ -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') },
];

View File

@@ -0,0 +1,2 @@
export * from './helpers';
export * from './formatVolume';