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

This commit is contained in:
Bryan1029384756
2026-02-14 01:57:15 -06:00
parent 6f12f98d30
commit 958cf56b23
51 changed files with 4761 additions and 1858 deletions

View File

@@ -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 && (