Skip to content

Pull Request to address issue #12#63

Open
arnavsoni1 wants to merge 17 commits intoACM-VIT:mainfrom
arnavsoni1:xc
Open

Pull Request to address issue #12#63
arnavsoni1 wants to merge 17 commits intoACM-VIT:mainfrom
arnavsoni1:xc

Conversation

@arnavsoni1
Copy link
Copy Markdown

While I was not able to add seed-generated-avatars, I have added an option to upload a profile picture that can be displayed when cameras are turned off.

Copilot AI review requested due to automatic review settings March 11, 2026 18:20
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 11, 2026

@arnavsoni1 is attempting to deploy a commit to the ACM-VIT's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for user-specified profile pictures (avatars) to be shown in place of video tiles when cameras are off, including pre-join upload flows and client-side rendering updates across web and mobile.

Changes:

  • Adds pre-join avatar upload/select UI (web + mobile) and persists the chosen avatar URL locally.
  • Plumbs avatarUrl through join flows and updates participant tile UIs to render avatars when video is unavailable.
  • Introduces an (unauthenticated) web API route to mint Cloudinary signed upload parameters.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/sfu/server/socket/handlers/joinRoom.ts Accepts avatarUrl on join, attempts to store/emit avatar info, emits avatarSnapshot.
packages/sfu/server/notifications.ts Extends userJoined payload to optionally include avatarUrl.
apps/web/src/app/meets-client.tsx Adds avatar state + resolver and wires into meeting UI.
apps/web/src/app/hooks/useMeetSocket.ts Sends avatarUrl during join and tracks avatars via userJoined/avatarSnapshot/avatarUpdated.
apps/web/src/app/hooks/useMeetRefs.ts Persists avatarUrl in joinOptionsRef.
apps/web/src/app/hooks/useMeetDisplayName.ts Adds avatarUrl into join options tracking.
apps/web/src/app/components/WhiteboardLayout.tsx Renders local/remote avatars when camera is off.
apps/web/src/app/components/PresentationLayout.tsx Renders local/remote avatars when camera is off.
apps/web/src/app/components/ParticipantVideo.tsx Adds per-participant avatar rendering fallback with load-failure handling.
apps/web/src/app/components/MeetsMainContent.tsx Threads avatar props through layout components and join screen.
apps/web/src/app/components/JoinScreen.tsx Implements pre-join avatar upload (Cloudinary signed upload with local data-url fallback) + persistence.
apps/web/src/app/components/GridLayout.tsx Renders local/remote avatars when camera is off.
apps/web/src/app/components/DevPlaygroundLayout.tsx Renders local/remote avatars when camera is off.
apps/web/src/app/components/BrowserLayout.tsx Renders local/remote avatars when camera is off.
apps/web/src/app/api/uploads/avatar/sign/route.ts New endpoint that generates Cloudinary upload signatures/params.
apps/web/README.md Documents Cloudinary env vars for avatar upload.
apps/mobile/src/features/meets/hooks/use-meet-socket.ts Sends avatarUrl during join and tracks avatars via socket events.
apps/mobile/src/features/meets/hooks/use-meet-refs.ts Persists avatarUrl in joinOptionsRef.
apps/mobile/src/features/meets/hooks/use-meet-display-name.ts Adds avatarUrl into join options tracking.
apps/mobile/src/features/meets/components/participant-tile.tsx Renders avatars for participants when video is unavailable.
apps/mobile/src/features/meets/components/meet-screen.tsx Adds avatar state + resolver and wires into meeting UI.
apps/mobile/src/features/meets/components/join-screen.tsx Implements avatar selection/upload + persistence on mobile (expo-image-picker + signed upload).
apps/mobile/src/features/meets/components/call-screen.tsx Uses resolved avatar URLs for strip tiles and participant tiles.
apps/mobile/package.json Adds expo-image-picker dependency.
Comments suppressed due to low confidence (1)

packages/sfu/server/socket/handlers/joinRoom.ts:10

  • AvatarSnapshot is imported from packages/sfu/types but that type does not exist there (verified packages/sfu/types.ts has no AvatarSnapshot export). This will fail TypeScript compilation; either add an AvatarSnapshot interface to the shared SFU types (and update any downstream package that re-exports them) or remove the import/satisfies AvatarSnapshot usage and type the payload locally.
import type {
  AvatarSnapshot,
  AppsAwarenessData,
  HandRaisedSnapshot,
  JoinRoomData,
  JoinRoomResponse,
} from "../../../types.js";

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 88 to 90
}
roomId = webinarTarget.roomId;
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handler reads data.avatarUrl, but JoinRoomData (from packages/sfu/types.ts) currently has no avatarUrl field. This will be a compile-time error and also leaves the join payload schema undocumented; please extend JoinRoomData to include avatarUrl?: string (and keep the web/mobile shared types in sync).

Suggested change
const avatarSource = data as any;
const hasAvatarOverride = typeof avatarSource?.avatarUrl === "string";
const avatarUrlCandidate =
typeof avatarSource?.avatarUrl === "string"
? avatarSource.avatarUrl.trim()
: "";

Copilot uses AI. Check for mistakes.
Comment on lines 88 to 91
}
roomId = webinarTarget.roomId;
}
const clientPolicy =
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avatarUrl is accepted from the client and forwarded/stored without any length or scheme validation. A malicious client could send a multi‑MB string (e.g., huge data URL) and bloat memory/network traffic for all participants. Add server-side validation (max length + allowlist schemes like https and/or data:image/jpeg;base64, with a strict size cap) and reject/trim invalid values before broadcasting.

Copilot uses AI. Check for mistakes.
Comment on lines 460 to +499
context.currentRoom = room;
context.currentRoom.setWebinarFeedRefreshNotifier((targetRoom) => {
emitWebinarFeedChanged(io, state, targetRoom);
});
context.pendingRoomId = null;
context.pendingRoomChannelId = null;
context.pendingUserKey = null;

if (isAdminJoin) {
context.currentClient = new Admin({
id: userId,
socket,
mode: isGhost ? "ghost" : "participant",
});
} else if (isWebinarAttendeeJoin) {
context.currentClient = new Client({
id: userId,
socket,
mode: "webinar_attendee",
});
} else {
context.currentClient = new Client({
id: userId,
socket,
mode: isGhost ? "ghost" : "participant",
});
}

context.currentRoom.setUserIdentity(userId, userKey, displayName, {
forceDisplayName: hasDisplayNameOverride,
});

const roomWithAvatarApis = context.currentRoom as typeof context.currentRoom & {
setUserAvatar?: (
userId: string,
userKey: string,
avatarUrl?: string,
options?: { forceAvatar?: boolean },
) => void;
getAvatarForUser?: (userId: string) => string | undefined;
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code attempts to persist and resolve avatars via setUserAvatar / getAvatarSnapshot / avatarByKey, but Room currently has no avatar-related fields or methods (verified packages/sfu/config/classes/Room.ts has no avatar*). As written, avatarByKey is always undefined and the fallback .set() calls are no-ops, so resolvedAvatarUrl/avatarSnapshot will always be empty/undefined for other users. Implement avatar storage + snapshot APIs on Room (mirroring displayNamesByKey + getDisplayNameSnapshot) or remove this wiring until the server supports it.

Copilot uses AI. Check for mistakes.
Comment on lines 575 to 581
userId: clientId,
displayName: ghostDisplayName,
avatarUrl: ghostAvatarUrl,
isGhost: true,
});
}
} else if (!context.currentClient.isWebinarAttendee) {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avatarSnapshot is emitted, but it will always contain an empty users array unless Room.getAvatarSnapshot exists (currently it does not). This means a newly joined client won’t learn avatars for participants already in the room. Once avatar storage exists on Room, ensure getAvatarSnapshot is implemented and used here (similar to getDisplayNameSnapshot).

Suggested change
users: context.currentRoom.getAvatarSnapshot({
includeGhosts: context.currentClient.isGhost,
includeWebinarAttendees: false,
}),

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +127
export async function POST(request: Request) {
const { cloudName, apiKey, apiSecret, folder } = resolveCloudinaryConfig();

if (!cloudName || !apiKey || !apiSecret) {
return Response.json(
{ error: "Avatar upload is not configured." },
{ status: 500 },
);
}

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signing endpoint is unauthenticated and can be called by anyone to obtain valid Cloudinary upload signatures, enabling anonymous abuse (storage/cost) against your Cloudinary account. Gate this route (e.g., require an authenticated session, a short-lived join token, and/or server-side rate limiting) and consider binding the signature to additional restrictive params (folder, allowed formats, max file size) so it can’t be used as a general-purpose uploader.

Suggested change
export async function POST(request: Request) {
const { cloudName, apiKey, apiSecret, folder } = resolveCloudinaryConfig();
if (!cloudName || !apiKey || !apiSecret) {
return Response.json(
{ error: "Avatar upload is not configured." },
{ status: 500 },
);
}
const resolveUploadSigningSecret = (): string | undefined => {
const env = process?.env ?? {};
return normalizeEnvValue(env.CLOUDINARY_AVATAR_UPLOAD_SECRET);
};
export async function POST(request: Request) {
const { cloudName, apiKey, apiSecret, folder } = resolveCloudinaryConfig();
const uploadSecret = resolveUploadSigningSecret();
if (!cloudName || !apiKey || !apiSecret || !uploadSecret) {
return Response.json(
{ error: "Avatar upload is not configured." },
{ status: 500 },
);
}
const authHeader =
request.headers.get("authorization") ??
request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const token = authHeader.slice("Bearer ".length).trim();
if (token !== uploadSecret) {
return Response.json({ error: "Forbidden" }, { status: 403 });
}

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +8
declare const process:
| {
env?: Record<string, string | undefined>;
}
| undefined;

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declare const process ... is unusual in a Node.js route and shadows the standard process typing; it can also mask configuration issues during development. Prefer using the normal process.env typing (or importing process from node:process if needed) and remove this declaration unless there’s a specific build-time constraint it’s addressing.

Suggested change
declare const process:
| {
env?: Record<string, string | undefined>;
}
| undefined;

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +18
- `CLOUDINARY_CLOUD_NAME`
- `CLOUDINARY_API_KEY`
- `CLOUDINARY_API_SECRET`
- optional: `CLOUDINARY_AVATAR_FOLDER` (default: `conclave/avatars`)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested list here uses tab indentation, which renders inconsistently in Markdown (often as a code block). Replace the tabs with spaces so the environment variable bullets render properly.

Suggested change
- `CLOUDINARY_CLOUD_NAME`
- `CLOUDINARY_API_KEY`
- `CLOUDINARY_API_SECRET`
- optional: `CLOUDINARY_AVATAR_FOLDER` (default: `conclave/avatars`)
- `CLOUDINARY_CLOUD_NAME`
- `CLOUDINARY_API_KEY`
- `CLOUDINARY_API_SECRET`
- optional: `CLOUDINARY_AVATAR_FOLDER` (default: `conclave/avatars`)

Copilot uses AI. Check for mistakes.
@theg1239
Copy link
Copy Markdown
Member

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bd85ad79e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +522 to +526
roomWithAvatarApis.userKeysById?.set(userId, userKey);
if (hasAvatarOverride) {
const normalizedAvatarUrl = requestedAvatarUrl?.trim();
if (normalizedAvatarUrl) {
roomWithAvatarApis.avatarByKey?.set(userKey, normalizedAvatarUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist avatars in room state before emitting join events

This fallback branch silently drops uploaded avatars in the current SFU implementation: Room does not provide setUserAvatar, getAvatarSnapshot, or an avatarByKey map, so avatarByKey?.set(...) is a no-op and no avatar is stored for userId. That means resolvedAvatarUrl and the emitted avatarSnapshot stay empty, so other participants never receive avatars even when a user uploads one successfully on the join screen.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants