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
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user