feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s

- Implemented Button component with various props for customization.
- Created Modal component with header, content, and footer subcomponents.
- Added Spinner component for loading indicators.
- Developed Toast component for displaying notifications.
- Introduced Tooltip component for contextual hints with keyboard shortcuts.
- Added corresponding CSS modules for styling each component.
- Updated index file to export new components.
- Configured TypeScript settings for the UI package.
This commit is contained in:
Bryan1029384756
2026-04-14 09:02:14 -05:00
parent 9ef839938e
commit b7a4cf4ce8
376 changed files with 52619 additions and 167641 deletions

View File

@@ -15,6 +15,10 @@ import deafenSound from '../assets/sounds/deafen.mp3';
import undeafenSound from '../assets/sounds/undeafen.mp3';
import viewerJoinSound from '../assets/sounds/screenshare_viewer_join.mp3';
import viewerLeaveSound from '../assets/sounds/screenshare_viewer_leave.mp3';
import screenshareStartSound from '../assets/sounds/screenshare_start.mp3';
import screenshareStopSound from '../assets/sounds/screenshare_stop.mp3';
import cameraOnSound from '../assets/sounds/camera_on.mp3';
import cameraOffSound from '../assets/sounds/camera_off.mp3';
const soundMap = {
join: joinSound,
@@ -25,6 +29,10 @@ const soundMap = {
undeafen: undeafenSound,
viewer_join: viewerJoinSound,
viewer_leave: viewerLeaveSound,
screenshare_start: screenshareStartSound,
screenshare_stop: screenshareStopSound,
camera_on: cameraOnSound,
camera_off: cameraOffSound,
};
const VoiceContext = createContext();
@@ -270,10 +278,33 @@ export const VoiceProvider = ({ children }) => {
setToken(lkToken);
const storedInputDevice = localStorage.getItem('voiceInputDevice');
const storedOutputDevice = localStorage.getItem('voiceOutputDevice');
const noiseSuppression = localStorage.getItem('voiceNoiseSuppression') !== 'false'; // default true
// Voice Settings tab (settings/UserSettingsModal.tsx) writes
// everything to a single `voiceSettings` JSON blob. Read it
// here so user choices actually flow through to LiveKit.
// Defaults: echoCancellation off (user preference — can be
// re-enabled in settings), everything else on.
let voiceSettings = {
inputDeviceId: 'default',
outputDeviceId: 'default',
videoDeviceId: 'default',
inputVolume: 100,
noiseSuppression: true,
echoCancellation: false,
autoGainControl: true,
};
try {
const raw = localStorage.getItem('voiceSettings');
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
voiceSettings = { ...voiceSettings, ...parsed };
}
}
} catch {
/* fall through to defaults */
}
const storedInputDevice = voiceSettings.inputDeviceId;
const storedOutputDevice = voiceSettings.outputDeviceId;
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
@@ -289,9 +320,9 @@ export const VoiceProvider = ({ children }) => {
],
},
audioCaptureDefaults: {
autoGainControl: true,
echoCancellation: true,
noiseSuppression,
autoGainControl: voiceSettings.autoGainControl,
echoCancellation: voiceSettings.echoCancellation,
noiseSuppression: voiceSettings.noiseSuppression,
channelCount: 1,
sampleRate: 48000,
...(storedInputDevice && storedInputDevice !== 'default' ? { deviceId: { exact: storedInputDevice } } : {}),
@@ -790,6 +821,48 @@ export const VoiceProvider = ({ children }) => {
prevViewersRef.current = currentViewers;
}, [voiceStates, watchingStreamOf]);
// Detect screen-share publications starting / stopping across the
// active voice channel (including the local user) and play a
// "stream started" / "stream stopped" SFX on transitions. Reuses
// the viewer_join / viewer_leave clips since they're already
// loaded and sound right for the event.
const prevSharersRef = useRef(new Set());
const sharerDetectionInitRef = useRef(false);
useEffect(() => {
if (!activeChannelId) {
prevSharersRef.current = new Set();
sharerDetectionInitRef.current = false;
return;
}
const channelUsers = voiceStates[activeChannelId] || [];
const currentSharers = new Set();
for (const u of channelUsers) {
if (u.isScreenSharing) currentSharers.add(u.userId);
}
if (!sharerDetectionInitRef.current) {
sharerDetectionInitRef.current = true;
prevSharersRef.current = currentSharers;
return;
}
const prev = prevSharersRef.current;
// Stream started — someone in the channel is now sharing
// who wasn't before.
for (const uid of currentSharers) {
if (!prev.has(uid)) {
playSound('screenshare_start');
break;
}
}
// Stream stopped — someone who was sharing isn't anymore.
for (const uid of prev) {
if (!currentSharers.has(uid)) {
playSound('screenshare_stop');
break;
}
}
prevSharersRef.current = currentSharers;
}, [voiceStates, activeChannelId]);
const disconnectVoice = () => {
console.log('User manually disconnected voice');
isDMCallRef.current = false;
@@ -824,11 +897,53 @@ export const VoiceProvider = ({ children }) => {
await updateVoiceState({ isDeafened: nextState });
};
// Actually flip the LiveKit screen-share publication on/off. The
// earlier implementation only toggled local React state + the
// voiceStates row, so the Share button lit up but no track ever
// got published. `setScreenShareEnabled` is LiveKit's one-call
// helper that prompts for `getDisplayMedia`, publishes the
// resulting track, and tears it down on false.
const setScreenSharing = async (active) => {
if (!room) return;
try {
await room.localParticipant.setScreenShareEnabled(active, {
audio: true,
});
} catch (e) {
console.warn('Failed to toggle screen share:', e);
// User cancelled the picker or permission was denied —
// keep local state in sync with whatever actually happened
// on the LiveKit side.
const published = !!room.localParticipant.getTrackPublication?.(
'screen_share',
);
setIsScreenSharingLocal(published);
await updateVoiceState({ isScreenSharing: published });
return;
}
setIsScreenSharingLocal(active);
await updateVoiceState({ isScreenSharing: active });
};
// Camera toggle — publishes / unpublishes the local webcam track
// via LiveKit's `setCameraEnabled` helper. Mirrors the mic and
// screen-share code paths so the UI can show a single consistent
// "active" state for each media type.
const [isCameraOn, setIsCameraOn] = useState(false);
const setCamera = async (active) => {
if (!room) return;
try {
await room.localParticipant.setCameraEnabled(active);
setIsCameraOn(active);
playSound(active ? 'camera_on' : 'camera_off');
} catch (e) {
console.warn('Failed to toggle camera:', e);
const published = !!room.localParticipant.isCameraEnabled;
setIsCameraOn(published);
}
};
const toggleCamera = () => setCamera(!isCameraOn);
const switchDevice = useCallback(async (kind, deviceId) => {
if (!room) return;
try {
@@ -856,6 +971,9 @@ export const VoiceProvider = ({ children }) => {
toggleDeafen,
isScreenSharing,
setScreenSharing,
isCameraOn,
setCamera,
toggleCamera,
personallyMutedUsers,
togglePersonalMute,
isPersonallyMuted,