feat: Implement core chat application UI, including chat, voice, members, DMs, and shared components.
Some checks failed
Build and Release / build-and-release (push) Failing after 0s
Some checks failed
Build and Release / build-and-release (push) Failing after 0s
This commit is contained in:
@@ -31,7 +31,10 @@ const VoiceContext = createContext();
|
||||
|
||||
export const useVoice = () => useContext(VoiceContext);
|
||||
|
||||
let _suppressAppSounds = false;
|
||||
|
||||
function playSound(type) {
|
||||
if (_suppressAppSounds) return;
|
||||
const src = soundMap[type];
|
||||
if (!src) return;
|
||||
const audio = new Audio(src);
|
||||
@@ -40,6 +43,7 @@ function playSound(type) {
|
||||
}
|
||||
|
||||
function playSoundUrl(url) {
|
||||
if (_suppressAppSounds) return;
|
||||
const audio = new Audio(url);
|
||||
audio.volume = 0.5;
|
||||
audio.play().catch(e => console.error("Sound play failed", e));
|
||||
@@ -60,6 +64,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
parseInt(localStorage.getItem('voiceOutputVolume') || '100')
|
||||
);
|
||||
const isMovingRef = useRef(false);
|
||||
const [isReceivingScreenShareAudio, setIsReceivingScreenShareAudio] = useState(false);
|
||||
|
||||
const convex = useConvex();
|
||||
|
||||
@@ -113,7 +118,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
// Apply volume to LiveKit participant (factoring in global output volume)
|
||||
const participant = room?.remoteParticipants?.get(userId);
|
||||
const globalVol = globalOutputVolume / 100;
|
||||
if (participant) participant.setVolume((volume / 100) * globalVol);
|
||||
if (participant) participant.setVolume(Math.min(1, (volume / 100) * globalVol));
|
||||
// Sync personal mute state
|
||||
if (volume === 0) {
|
||||
setPersonallyMutedUsers(prev => {
|
||||
@@ -147,7 +152,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
const vol = userVolumes[userId] ?? 100;
|
||||
const restoreVol = vol === 0 ? 100 : vol;
|
||||
const participant = room?.remoteParticipants?.get(userId);
|
||||
if (participant) participant.setVolume((restoreVol / 100) * globalVol);
|
||||
if (participant) participant.setVolume(Math.min(1, (restoreVol / 100) * globalVol));
|
||||
// Update stored volume if it was 0
|
||||
if (vol === 0) {
|
||||
setUserVolumes(p => {
|
||||
@@ -178,6 +183,16 @@ export const VoiceProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const disconnectUser = async (targetUserId) => {
|
||||
const actorUserId = localStorage.getItem('userId');
|
||||
if (!actorUserId) return;
|
||||
try {
|
||||
await convex.mutation(api.voiceState.disconnectUser, { actorUserId, targetUserId });
|
||||
} catch (e) {
|
||||
console.error('Failed to disconnect user:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const isServerMuted = (userId) => {
|
||||
for (const users of Object.values(voiceStates)) {
|
||||
const user = users.find(u => u.userId === userId);
|
||||
@@ -197,7 +212,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
);
|
||||
|
||||
// Refs for detecting other-user joins via voiceStates changes
|
||||
const prevChannelUsersRef = useRef(new Map());
|
||||
const prevChannelUsersRef = useRef(new Set());
|
||||
const otherJoinInitRef = useRef(false);
|
||||
const isInAfkChannel = !!(activeChannelId && serverSettings?.afkChannelId === activeChannelId);
|
||||
|
||||
@@ -378,7 +393,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
participant.setVolume(0);
|
||||
} else {
|
||||
const userVol = (userVolumes[identity] ?? 100) / 100;
|
||||
participant.setVolume(userVol * globalVol);
|
||||
participant.setVolume(Math.min(1, userVol * globalVol));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -418,31 +433,34 @@ export const VoiceProvider = ({ children }) => {
|
||||
// Detect other users joining the same voice channel and play their join sound
|
||||
useEffect(() => {
|
||||
if (!activeChannelId) {
|
||||
prevChannelUsersRef.current = new Map();
|
||||
prevChannelUsersRef.current = new Set();
|
||||
otherJoinInitRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const selfId = localStorage.getItem('userId');
|
||||
const channelUsers = voiceStates[activeChannelId] || [];
|
||||
const currentUsers = new Map();
|
||||
for (const u of channelUsers) {
|
||||
currentUsers.set(u.userId, u);
|
||||
const currentUserIds = new Set(channelUsers.map(u => u.userId));
|
||||
|
||||
// Guard: ignore transient empty states when we previously had users
|
||||
if (currentUserIds.size === 0 && prevChannelUsersRef.current.size > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip the first render after joining to avoid playing sounds for users already in the channel
|
||||
if (!otherJoinInitRef.current) {
|
||||
otherJoinInitRef.current = true;
|
||||
prevChannelUsersRef.current = currentUsers;
|
||||
prevChannelUsersRef.current = currentUserIds;
|
||||
return;
|
||||
}
|
||||
|
||||
const prev = prevChannelUsersRef.current;
|
||||
const prevIds = prevChannelUsersRef.current;
|
||||
|
||||
// Detect new users (not self)
|
||||
for (const [uid, userData] of currentUsers) {
|
||||
if (uid !== selfId && !prev.has(uid)) {
|
||||
if (userData.joinSoundUrl) {
|
||||
for (const uid of currentUserIds) {
|
||||
if (uid !== selfId && !prevIds.has(uid)) {
|
||||
const userData = channelUsers.find(u => u.userId === uid);
|
||||
if (userData?.joinSoundUrl) {
|
||||
playSoundUrl(userData.joinSoundUrl);
|
||||
} else {
|
||||
playSound('join');
|
||||
@@ -451,7 +469,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
prevChannelUsersRef.current = currentUsers;
|
||||
prevChannelUsersRef.current = currentUserIds;
|
||||
}, [voiceStates, activeChannelId]);
|
||||
|
||||
// Manage screen share subscriptions — only subscribe when actively watching
|
||||
@@ -459,6 +477,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
if (!room) return;
|
||||
|
||||
const manageSubscriptions = () => {
|
||||
let receivingAudio = false;
|
||||
for (const p of room.remoteParticipants.values()) {
|
||||
const { screenSharePub, screenShareAudioPub } = findTrackPubs(p);
|
||||
|
||||
@@ -470,7 +489,13 @@ export const VoiceProvider = ({ children }) => {
|
||||
if (screenShareAudioPub && screenShareAudioPub.isSubscribed !== shouldSubscribe) {
|
||||
screenShareAudioPub.setSubscribed(shouldSubscribe);
|
||||
}
|
||||
|
||||
if (shouldSubscribe && screenShareAudioPub && screenShareAudioPub.isSubscribed) {
|
||||
receivingAudio = true;
|
||||
}
|
||||
}
|
||||
_suppressAppSounds = receivingAudio;
|
||||
setIsReceivingScreenShareAudio(receivingAudio);
|
||||
};
|
||||
|
||||
manageSubscriptions();
|
||||
@@ -478,10 +503,14 @@ export const VoiceProvider = ({ children }) => {
|
||||
const onTrackChange = () => manageSubscriptions();
|
||||
room.on(RoomEvent.TrackPublished, onTrackChange);
|
||||
room.on(RoomEvent.TrackSubscribed, onTrackChange);
|
||||
room.on(RoomEvent.TrackUnsubscribed, onTrackChange);
|
||||
|
||||
return () => {
|
||||
room.off(RoomEvent.TrackPublished, onTrackChange);
|
||||
room.off(RoomEvent.TrackSubscribed, onTrackChange);
|
||||
room.off(RoomEvent.TrackUnsubscribed, onTrackChange);
|
||||
_suppressAppSounds = false;
|
||||
setIsReceivingScreenShareAudio(false);
|
||||
};
|
||||
}, [room, watchingStreamOf]);
|
||||
|
||||
@@ -639,6 +668,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
setUserVolume,
|
||||
getUserVolume,
|
||||
serverMute,
|
||||
disconnectUser,
|
||||
isServerMuted,
|
||||
isInAfkChannel,
|
||||
serverSettings,
|
||||
@@ -647,6 +677,7 @@ export const VoiceProvider = ({ children }) => {
|
||||
switchDevice,
|
||||
globalOutputVolume,
|
||||
setGlobalOutputVolume,
|
||||
isReceivingScreenShareAudio,
|
||||
}}>
|
||||
{children}
|
||||
{room && (
|
||||
|
||||
Reference in New Issue
Block a user