Skip to content

Conversation

@lismarreiros
Copy link

@lismarreiros lismarreiros commented Oct 20, 2025

…g in DeviceCheck component

🌟 Pre-submission Checklist

  • ⭐ I have starred the repository (mandatory before contributing)
  • πŸ’¬ I am a member of the Discord server: Join Here
  • πŸ“ I have signed up at helixque.netlify.app
  • πŸ“’ I have checked the #pull-request channel to ensure no one else is working on this issue
  • πŸ“ I have mentioned this PR in the Discord #pull-request channel

Summary

Brief description of what this PR accomplishes.

Type of Changes

  • πŸš€ Feature addition
  • πŸ› Bug fix
  • πŸ“š Documentation update
  • πŸ”§ Refactoring
  • 🎨 UI/UX improvements
  • ⚑ Performance optimizations
  • πŸ“± Mobile responsiveness
  • β™Ώ Accessibility improvements
  • Other: _____

Testing Completed

  • βœ… I have tested these changes locally
  • πŸ”§ Backend functionality works properly (if applicable)
  • 🎨 Frontend functionality works properly (if applicable)
  • 🌐 WebRTC connections work properly (if applicable)
  • πŸ“± Tested on different screen sizes/devices
  • πŸ”„ Tested edge cases (disconnections, reconnections, etc.)
  • πŸ§ͺ All existing functionality remains unaffected

Development Setup Verification

  • πŸ“¦ Dependencies installed for both frontend and backend
  • πŸš€ Development servers start without errors
  • πŸ—οΈ Code builds successfully

Code Quality

  • πŸ“ Follows existing TypeScript and React patterns
  • πŸ“ Uses meaningful variable and function names
  • πŸ’‘ Added comments for complex logic
  • 🎯 Code is properly formatted
  • πŸ” Self-review of the code has been performed

Related Issues

Closes #168

Screenshots/Videos

Screenshot 2025-10-20 at 10 40 38 Screenshot 2025-10-20 at 10 40 24 Screenshot 2025-10-20 at 17 04 23

Additional Notes

Any additional information or context about the changes.


Note: For faster PR review and approval, ensure you're active in our Discord server!

Summary by CodeRabbit

  • New Features

    • Upload and manage a custom avatar used as your video placeholder when camera is off.
    • Change avatar from the device-check UI; avatar now appears throughout the room and video views.
  • Improvements

    • More reliable camera/microphone acquisition flow tied to device on/off state.
    • Better permission handling and cleanup, including proper removal of temporary avatar data to avoid stale images.

@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

Walkthrough

Adds a session-scoped avatar upload and UI; threads an optional avatar?: string | null prop Room β†’ VideoGrid; displays avatar (or fallback icon) when local camera is off; updates DeviceCheck to manage avatar object URLs, gate getUserMedia by video/audio toggles, and clean up media tracks and URLs.

Changes

Cohort / File(s) Summary
Avatar state & UI
frontend/components/RTC/DeviceCheck.tsx
Adds avatar state, file input uploader (handleAvatarChange) with type/size validation, creates/revokes blob URLs, shows avatar (or user icon) in video placeholder, and exposes avatar prop to Room. Includes React default import.
Prop typing & forwarding
frontend/components/RTC/Room.tsx
Adds `avatar?: string
Avatar-driven rendering
frontend/components/RTC/VideoGrid.tsx
Adds `avatar?: string
Media acquisition & permission handling
frontend/components/RTC/DeviceCheck.tsx
Refactors getCam flow to gate navigator.mediaDevices.getUserMedia by videoOn/audioOn, store tracks in refs/state, attach preview stream, maintain permission-watcher effects, and perform cleanup of tracks and avatar object URLs on change/unmount.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant DeviceCheck as DeviceCheck (UI)
  participant Room as Room (container)
  participant VideoGrid as VideoGrid (render)
  note right of DeviceCheck #E8F5E9: avatar state + media refs

  User->>DeviceCheck: upload avatar file
  DeviceCheck->>DeviceCheck: validate β†’ create blob URL β†’ set `avatar`
  DeviceCheck->>DeviceCheck: revoke previous blob URL (if any)

  User->>DeviceCheck: toggle video/audio
  DeviceCheck->>Browser: if videoOn/audioOn β†’ getUserMedia
  alt stream returned
    Browser-->>DeviceCheck: MediaStream
    DeviceCheck->>DeviceCheck: store tracks in refs/state
    DeviceCheck->>VideoElement: attach stream for preview
  else error/denied
    Browser-->>DeviceCheck: error
    DeviceCheck->>User: show toast/error
  end

  DeviceCheck->>Room: render with `avatar` prop
  Room->>VideoGrid: forward `avatar`
  VideoGrid->>VideoGrid: if camOff β†’ render `avatar` or fallback icon
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

enhancement

Poem

🐰 I stitched a tiny face from light,

A session-spark to hide the night.
When cameras sleep, my portrait hops,
A carrot grin that never stops. πŸ₯•

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description follows the repository template structure and includes most required sections with checkmarks and selections. However, the "Summary" sectionβ€”which is meant to provide a brief description of what the PR accomplishesβ€”is completely unfilled, containing only the template placeholder text "Brief description of what this PR accomplishes." While other sections such as Type of Changes, Testing Completed, Code Quality, and Related Issues are properly populated with checkmarks and relevant information (including three screenshots), the missing Summary section represents an important gap in documenting the PR's intent.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
βœ… Passed checks (3 passed)
Check name Status Explanation
Title Check βœ… Passed The pull request title "Add avatar upload functionality" clearly and concisely describes the primary change introduced in this PR. The title directly maps to the main feature being implemented as described in the raw summary and linked issue #168. The title is specific, uses clear language, and avoids vague terms or noise, making it easily understandable when scanning git history. It accurately reflects the core objective of enabling users to upload custom avatars.
Linked Issues Check βœ… Passed The code changes align well with all acceptance criteria from linked issue #168. The implementation adds avatar upload functionality with client-side validation of image type and size (satisfying criterion 1), displays the chosen avatar when the camera is off in both the video placeholder and main video regions (criterion 2), provides fallback rendering to a default user icon when no avatar is selected (criterion 3), and maintains client-side-only state management with proper cleanup of blob URLs on unmount, ensuring avatars do not persist across page refreshes or sessions (criterion 4). All changes in DeviceCheck.tsx, Room.tsx, and VideoGrid.tsx directly support these requirements.
Out of Scope Changes Check βœ… Passed All code changes in this PR are directly aligned with the scope of issue #168. The modifications to DeviceCheck.tsx (avatar upload handler and UI), Room.tsx (avatar prop threading), and VideoGrid.tsx (avatar display logic) are all necessary and sufficient to implement the avatar upload feature. Supporting refactorings such as adjusting effects, maintaining permission watcher structure, and separating avatar logic from camera flow are appropriately scoped as they enable proper integration of the avatar functionality without introducing unrelated changes. No out-of-scope modifications have been introduced.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
frontend/components/RTC/DeviceCheck.tsx (6)

115-118: Revoke avatar blob URL on unmount/change

Avoid leaking object URLs when the user changes/leaves.

 useEffect(() => {
   getCamRef.current = getCam;
 });
 
+useEffect(() => {
+  return () => {
+    if (avatar?.startsWith("blob:")) URL.revokeObjectURL(avatar);
+  };
+}, [avatar]);

39-76: Reduce prompts: request audio+video in a single getUserMedia when both are on

Two sequential getUserMedia calls can double‑prompt and complicate device selection. Request both in one call when needed.

-      // request camera stream only if videoOn is true
-      if (videoOn) {
-        try {
-          const videoStream = await navigator.mediaDevices.getUserMedia({ video: true });
-          videoTrack = videoStream.getVideoTracks()[0] || null;
-        } catch (err) {
-          console.warn("Camera access denied or unavailable:", err);
-          toast.error("Camera Error", { description: "Could not access camera" });
-        }
-      }
-      //  Request microphone stream only if audioOn is true
-      if (audioOn) {
-        try {
-          const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
-          audioTrack = audioStream.getAudioTracks()[0] || null;
-        } catch (err) {
-          console.warn("Microphone access denied or unavailable:", err);
-          toast.error("Microphone Error", { description: "Could not access microphone" });
-        }
-      }
+      // Request in one call when possible to reduce prompts
+      if (videoOn || audioOn) {
+        try {
+          const constraints: MediaStreamConstraints = {
+            video: !!videoOn,
+            audio: audioOn ? { echoCancellation: true, noiseSuppression: true, autoGainControl: true } : false,
+          };
+          const stream = await navigator.mediaDevices.getUserMedia(constraints);
+          videoTrack = stream.getVideoTracks()[0] || null;
+          audioTrack = stream.getAudioTracks()[0] || null;
+        } catch (err) {
+          console.warn("Media access denied or unavailable:", err);
+          toast.error("Device Access Error", { description: "Could not access camera and/or microphone" });
+        }
+      }

89-109: Consider also watching microphone permissions and device changes

Add microphone to the Permissions watcher where supported, and listen to navigator.mediaDevices.devicechange to re‑probe devices when hardware changes.

Example:

try {
  const mic = await navigator.permissions.query({ name: "microphone" as PermissionName });
  mic.onchange = () => mic.state === "granted" && getCamRef.current();
} catch {}
navigator.mediaDevices?.addEventListener?.("devicechange", () => getCamRef.current());

2-2: Type‑only import; drop default React import

You only need React for the event type. Prefer a type‑only import to avoid an unnecessary runtime binding. Based on learnings.

-import React, { useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
+import type { ChangeEvent } from "react";
-const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+const handleAvatarChange = (e: ChangeEvent<HTMLInputElement>) => {

Also applies to: 30-30


120-130: Optionally reset avatar on leave to match β€œresets when leaving the session”

If β€œleaves the session” means leaving the meeting, clear the avatar here (and revoke blob URL).

   const handleOnLeave = () => {
     setJoined(false);
     try {
       localAudioTrack?.stop();
     } catch {}
     try {
       localVideoTrack?.stop();
     } catch {}
     setLocalAudioTrack(null);
     setLocalVideoTrack(null);
+    if (avatar?.startsWith("blob:")) URL.revokeObjectURL(avatar);
+    setAvatar(null);
   };

171-191: UI looks good; small a11y/perf nit

Nice placeholder UX. Consider adding decoding="async" to the img for quicker paint; optional.

-  <img 
-    src={avatar}
-    alt="Avatar"
-    className="h-24 w-24 rounded-full object-cover border border-white/20"
-  />
+  <img 
+    src={avatar}
+    alt="Avatar"
+    decoding="async"
+    className="h-24 w-24 rounded-full object-cover border border-white/20"
+  />
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 59f3f45 and 69905bc.

πŸ“’ Files selected for processing (1)
  • frontend/components/RTC/DeviceCheck.tsx (5 hunks)
πŸ”‡ Additional comments (2)
frontend/components/RTC/DeviceCheck.tsx (2)

25-27: LGTM: track refs + cleanup prevent leaks

Using refs for current tracks and stopping them on cleanup is solid and avoids dangling device streams.

Also applies to: 104-109


132-140: Avatar not passed to Room component during call

The review identifies a real implementation gap: DeviceCheck uses an avatar in the preview UI, but doesn't pass it to the Room component, which has no avatar prop or fallback UI when video is off.

The suggested change is technically correctβ€”adding avatar={avatar} to the Room component props would require:

  1. Adding avatar?: string | null to the RoomProps interface
  2. Accepting it in the Room function signature
  3. Using it in the video-off rendering logic (likely within VideoGrid)

However, I cannot verify from the codebase whether avatar display during active calls is an actual acceptance requirement. You should manually confirm:

  • Whether the feature spec requires showing avatars when participants disable their camera during the call
  • How the VideoGrid component should integrate avatar display when video is off

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
frontend/components/RTC/DeviceCheck.tsx (1)

29-36: Harden avatar uploads: validate MIME/size, block SVG, use blob URLs, and revoke them

Current code accepts image/*, reads as Data URL, and lacks validation. This risks large in-memory strings and potential SVG-based surprises. Validate type/size, avoid SVG, prefer object URLs, and revoke old URLs to prevent leaks.

Apply:

-// Handle avatar upload
-const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-  const file = e.target.files?.[0];
-  if (!file) return;
-  const reader = new FileReader();
-  reader.onloadend = e => setAvatar(e.target?.result as string);
-  reader.readAsDataURL(file);
-};
+// Handle avatar upload (validates + uses object URL)
+const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  const file = e.target.files?.[0];
+  if (!file) return;
+  const allowed = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
+  const MAX_MB = 5;
+  if (!allowed.has(file.type)) {
+    toast.error("Unsupported file type", { description: "Use PNG, JPEG, WebP, or GIF." });
+    e.currentTarget.value = "";
+    return;
+  }
+  if (file.size > MAX_MB * 1024 * 1024) {
+    toast.error("File too large", { description: `Max size is ${MAX_MB}MB.` });
+    e.currentTarget.value = "";
+    return;
+  }
+  const url = URL.createObjectURL(file);
+  setAvatar(prev => {
+    if (prev?.startsWith("blob:")) URL.revokeObjectURL(prev);
+    return url;
+  });
+};

And tighten the input:

-<input 
-  type="file" 
-  accept="image/*" 
-  onChange={handleAvatarChange} 
-  className="hidden" 
-/>
+<input
+  type="file"
+  accept="image/png,image/jpeg,image/webp,image/gif"
+  onChange={handleAvatarChange}
+  className="hidden"
/>

Also applies to: 184-189

🧹 Nitpick comments (6)
frontend/components/RTC/Room.tsx (2)

12-18: Avoid duplicate β€œensureRemoteStream” logic; keep a single source

You import ensureRemoteStream but also define ensureRemoteStreamLocal with overlapping behavior. Prefer one to prevent drift. If the local helper is preferred, drop the import; otherwise, reuse the util and delete the local helper.

Example (drop unused import):

-import { 
-  ensureRemoteStream, 
-  detachLocalPreview, 
+import { 
+  detachLocalPreview, 
   stopProvidedTracks, 
   teardownPeers,
   toggleCameraTrack
 } from "./webrtc-utils";

Also applies to: 81-96


218-221: Don’t request screen-share audio if unused

getDisplayMedia asks for audio but you never attach the audio track, causing extra permission prompts. Request video-only.

-const screenStream = await navigator.mediaDevices.getDisplayMedia({
-  video: true,
-  audio: true
-});
+const screenStream = await navigator.mediaDevices.getDisplayMedia({
+  video: true,
+  audio: false
+});
frontend/components/RTC/DeviceCheck.tsx (3)

115-118: Revoke object URLs on unmount/change to prevent memory leaks

Add a cleanup effect to revoke the current blob URL when the component unmounts or when avatar changes.

 useEffect(() => {
   getCamRef.current = getCam;
 });
+
+// Revoke blob URL when replaced/unmounted
+useEffect(() => {
+  return () => {
+    if (avatar?.startsWith("blob:")) {
+      try { URL.revokeObjectURL(avatar); } catch {}
+    }
+  };
+}, [avatar]);

45-64: Optional: consolidate getUserMedia calls to reduce prompts

When both toggles are on, one combined call is simpler and can reduce permission prompts; split only if needed.

Sketch:

- let videoTrack: MediaStreamTrack | null = null;
- let audioTrack: MediaStreamTrack | null = null;
- if (videoOn) { /* get video */ }
- if (audioOn) { /* get audio */ }
+ let videoTrack: MediaStreamTrack | null = null;
+ let audioTrack: MediaStreamTrack | null = null;
+ if (videoOn || audioOn) {
+   const stream = await navigator.mediaDevices.getUserMedia({ video: videoOn, audio: audioOn });
+   videoTrack = videoOn ? stream.getVideoTracks()[0] ?? null : null;
+   audioTrack = audioOn ? stream.getAudioTracks()[0] ?? null : null;
+}

89-110: Optional: watch microphone permission too

You watch β€œcamera” permission; mirroring for β€œmicrophone” improves UX when users grant mic mid-flow. Guard with feature detection as you did.

frontend/components/RTC/VideoGrid.tsx (1)

68-72: Minor a11y/UX polish for avatar images

Consider setting alt to a more descriptive string (e.g., ${name || "You"}’s avatar) and add draggable={false} to prevent accidental drag on desktop.

Example:

-<img src={avatar} alt="Avatar" className="absolute inset-0 h-full w-full object-cover" />
+<img src={avatar} alt={`${name || "You"}'s avatar`} draggable={false} className="absolute inset-0 h-full w-full object-cover" />

Also applies to: 201-204

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 69905bc and cf6a201.

πŸ“’ Files selected for processing (3)
  • frontend/components/RTC/DeviceCheck.tsx (6 hunks)
  • frontend/components/RTC/Room.tsx (3 hunks)
  • frontend/components/RTC/VideoGrid.tsx (4 hunks)
πŸ”‡ Additional comments (2)
frontend/components/RTC/Room.tsx (1)

22-30: Avatar prop wiring looks good

Prop is typed, plumbed through Room, and forwarded to VideoGrid correctly. No concerns.

Also applies to: 32-40, 756-768

frontend/components/RTC/VideoGrid.tsx (1)

33-34: Avatar fallback rendering is correct and consistent

Prop is typed and consumed correctly; avatar shows when camOff, with sensible default icon fallback. Nicely done.

Also applies to: 46-48, 57-78, 188-210

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
frontend/components/RTC/DeviceCheck.tsx (1)

52-101: LGTM! Media acquisition refactored cleanly.

The separate video/audio acquisition logic with individual error handling provides better UX by showing specific error messages for camera vs. microphone failures.

Optional: The outer try-catch (lines 95-100) is now largely redundant since all operations within are wrapped in their own try-catches. Consider removing it for clarity:

 const getCam = async () => {
-  try {
     localAudioTrackRef.current?.stop();
     localVideoTrackRef.current?.stop();
     let videoTrack: MediaStreamTrack | null = null;
     let audioTrack: MediaStreamTrack | null = null;
     
     // ... rest of function
     
     if (!videoOn && !audioOn && videoRef.current) {
       videoRef.current.srcObject = null;
     }
-  } catch (e: any) {
-    const errorMessage = e?.message || "Could not access camera/microphone";
-    toast.error("Device Access Error", {
-      description: errorMessage
-    });
-  }
 };
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between cf6a201 and 3113c18.

πŸ“’ Files selected for processing (2)
  • frontend/components/RTC/DeviceCheck.tsx (6 hunks)
  • frontend/components/RTC/VideoGrid.tsx (4 hunks)
πŸ”‡ Additional comments (3)
frontend/components/RTC/VideoGrid.tsx (1)

33-33: LGTM! Avatar prop integration is clean.

The optional avatar prop is correctly typed and threaded through the component to support avatar display when the camera is off.

Also applies to: 46-47

frontend/components/RTC/DeviceCheck.tsx (2)

103-132: LGTM! Effects properly structured with cleanup.

The refactored effects cleanly separate concerns:

  • Permission watching with cleanup (lines 103-123)
  • Media acquisition on toggle (lines 125-127)
  • Latest getCam reference for callbacks (lines 129-131)

The getCamRef pattern ensures the permission watcher always calls the current getCam closure, avoiding stale dependencies.


186-206: LGTM! Avatar preview UI is well-designed.

The avatar display when video is off is clear and user-friendly:

  • Circular avatar with standard Tailwind sizing (h-24 w-24)
  • Intuitive upload/change button
  • File input restricted to validated MIME types matching the handler

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
frontend/components/RTC/DeviceCheck.tsx (2)

2-2: Remove unnecessary React default import.

With React 19's automatic JSX transform, the default React import is no longer required. Only the named hook imports are needed.

Apply this diff:

-import React, { useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";

Based on learnings


129-131: Consider adding a comment to clarify the no-dependency pattern.

This effect intentionally runs on every render to keep getCamRef.current pointing to the latest getCam closure. This avoids stale closures in the permission watcher (line 110), which is set up once with empty deps. The pattern is correct but unconventional.

Optionally add a clarifying comment:

+// Update ref to latest getCam to avoid stale closures in permission watcher
 useEffect(() => {
   getCamRef.current = getCam;
 });
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3113c18 and 748a58a.

πŸ“’ Files selected for processing (2)
  • frontend/components/RTC/DeviceCheck.tsx (6 hunks)
  • frontend/components/RTC/VideoGrid.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/components/RTC/VideoGrid.tsx
πŸ”‡ Additional comments (5)
frontend/components/RTC/DeviceCheck.tsx (5)

133-139: Cleanup effect correctly handles blob URL lifecycle.

The effect properly revokes blob URLs on avatar changes and unmount, addressing the previous review concern. Once the inline revocation in handleAvatarChange is removed, this will be the single, correct cleanup point.


52-101: LGTM: getCam refactoring correctly gates device requests.

The refactoring properly:

  • Requests video/audio only when respective toggle is on
  • Handles device errors independently
  • Stops old tracks before acquiring new ones
  • Clears the video stream when both devices are off

This improves efficiency and user experience.


194-214: LGTM: Avatar UI is accessible and user-friendly.

The implementation:

  • Shows avatar/icon only when video is off (correct per requirements)
  • Uses accessible label + hidden input pattern
  • Provides clear user guidance ("Upload Avatar" vs "Change Avatar")
  • File accept attribute matches validation rules

Well done!


33-44: LGTM: Validation is thorough and user-friendly.

The validation logic properly:

  • Restricts to safe, common image formats
  • Enforces reasonable 5MB size limit
  • Provides clear, actionable error messages
  • Resets input to allow re-upload after errors

Excellent error handling!


161-161: LGTM: Avatar prop correctly passed to Room.

The avatar is properly threaded through the component hierarchy as string | null, enabling display in VideoGrid when the camera is off.

@jayanthjamin-web
Copy link
Member

@lismarreiros join in another room and check the states are updating or not

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.

Allow users to choose a custom avatar and display it when the camera is off

2 participants