Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pop-out-clock proof of concept #1247

Merged
merged 2 commits into from
Oct 18, 2024
Merged

Conversation

Haavard15
Copy link

Very rough proof of concept for Video PiP that can pop out on top of presentation notes etc.

Copy link
Contributor

coderabbitai bot commented Oct 8, 2024

Walkthrough

The changes in this pull request introduce a new PopOutTimer component to the AppRouter, which is lazily loaded and displayed within a React.Suspense fallback. Additionally, a new configuration file, PopOutTimer.options.ts, defines various customizable options for the timer's appearance and behavior. The styles for the timer are specified in PopOutTimer.scss, while the functionality for the timer is encapsulated in the PopOutTimer.tsx component, which includes features like Picture-in-Picture (PiP) mode and canvas rendering.

Changes

File Path Change Summary
apps/client/src/AppRouter.tsx Added PopOutTimer component with props isMirrored and time, and imported types from ontime-types.
apps/client/src/features/viewers/pop-out-clock/PopOutTimer.options.ts Introduced MINIMAL_TIMER_OPTIONS, an array of configuration options for the timer's appearance and behavior.
apps/client/src/features/viewers/pop-out-clock/PopOutTimer.scss Created styles for the PopOutTimer, including responsive design and state-based styling.
apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx Added PopOutClock component with props for timer display and PiP functionality, utilizing React hooks.

Possibly related issues

  • Pop out clock #1213: The changes implement a pop-out clock feature that aligns with the request to create a video blob from a canvas and utilize the Picture-in-Picture functionality, as described in the issue.

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@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: 8

🧹 Outside diff range and nitpick comments (9)
apps/client/src/features/viewers/pop-out-clock/PopOutTimer.scss (4)

1-15: LGTM! Consider using viewport units for width.

The import and main container styles are well-structured and follow best practices. The use of CSS variables and Grid layout is commendable.

For improved responsiveness, consider using width: 100vw; instead of width: 100%; to ensure the timer spans the full viewport width, including any potential scrollbar space.


16-20: Specify transition properties for better performance.

The finished state styles are well-implemented, especially the use of clamp() for responsive sizing.

To optimize performance, specify which properties should transition:

-    transition: $viewer-transition-time;
+    transition: outline $viewer-transition-time, outline-offset $viewer-transition-time;

This change ensures only the relevant properties are animated, potentially improving rendering performance.


22-41: LGTM! Consider adding a minimum font size for better readability.

The timer styles are well-implemented, with good use of CSS variables and responsive units.

To ensure readability on very small screens, consider adding a minimum font size:

-    font-size: 20vw;
+    font-size: clamp(24px, 20vw, 200px);

This change ensures the timer text doesn't become too small on narrow viewports while maintaining its responsive nature.


43-53: LGTM! Consider using a CSS variable for consistency.

The end message styles are well-implemented, providing a clear and prominent display when the timer ends.

For consistency with the timer styles and easier customization, consider using a CSS variable for the color:

-      color: $timer-finished-color;
+      color: var(--timer-finished-color-override, $timer-finished-color);

This change allows for easy overriding of the end message color while maintaining the default from the SCSS variable.

apps/client/src/features/viewers/pop-out-clock/PopOutTimer.options.ts (2)

5-21: Consider clarifying the "Hide Overtime" description.

The "Timer Options" and "Element visibility" sections are well-organized. However, the description for the "Hide Overtime" option could be more precise.

Consider updating the description to clarify that it affects both visual styles:

-    description: 'Whether to suppress overtime styles (red borders and red text)',
+    description: 'Whether to suppress overtime styles (hides both red borders and red text)',

This change makes it explicit that both visual elements are affected by this option.


54-91: LGTM: Comprehensive positioning options with a minor suggestion.

The alignment and offset options provide excellent control over the timer's appearance and position. The use of predefined values for alignment enhances usability.

Consider updating the vertical alignment description for accuracy:

-    description: 'Moves the vertically in page to start = left | center | end = right',
+    description: 'Moves vertically in page to start = top | center | end = bottom',

This change corrects the directional terms for vertical alignment, improving clarity for users.

apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx (3)

129-132: Remove unused variable i

The variable i is initialized and incremented inside the interval but is not used elsewhere in the code. This is unnecessary and can be safely removed.

Apply this diff to remove the unused variable:

- let i = 0;
  const interval = setInterval(() => {
    changeVideo("green", display, context!, canvas, videoRef.current!);
-   i++;
  }, 1000);

10-12: Prop isMirrored is not used; consider removing it

The isMirrored prop is defined in MinimalTimerProps and extracted in the component but is not used anywhere. If mirroring functionality is intended, please implement it. Otherwise, consider removing this prop to simplify the component.

Apply this diff to remove the unused prop:

- interface MinimalTimerProps {
-   isMirrored: boolean;
    time: ViewExtendedTimer;
    viewSettings: ViewSettings;
  }
...
- const { isMirrored, time, viewSettings } = props;
+ const { time, viewSettings } = props;

50-68: Variable clicked is not used; consider removing it

The clicked variable is set to true when openPip is called but isn't used elsewhere in the component. If it's intended for future use, please implement the functionality. Otherwise, consider removing it to clean up the code.

Apply this diff to remove the unused variable:

- let clicked = false;
...
  const openPip = async () => {
    if (!videoRef.current) return;
-   clicked = true;
    await videoRef.current.play();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 86e6f8b and 8085ffe.

📒 Files selected for processing (4)
  • apps/client/src/AppRouter.tsx (2 hunks)
  • apps/client/src/features/viewers/pop-out-clock/PopOutTimer.options.ts (1 hunks)
  • apps/client/src/features/viewers/pop-out-clock/PopOutTimer.scss (1 hunks)
  • apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx (1 hunks)
🧰 Additional context used
🔇 Additional comments (8)
apps/client/src/features/viewers/pop-out-clock/PopOutTimer.scss (1)

1-53: Overall, excellent implementation with room for minor enhancements.

The PopOutTimer.scss file demonstrates a well-structured, responsive, and customizable approach to styling the pop-out clock component. It effectively uses SCSS features, CSS variables, and modern layout techniques like CSS Grid. The suggested improvements are minor and focus on enhancing consistency, accessibility, and performance.

Great job on creating a flexible and visually appealing style for the timer component!

apps/client/src/features/viewers/pop-out-clock/PopOutTimer.options.ts (2)

1-4: LGTM: Imports and constant declaration are appropriate.

The imports and the constant declaration are well-structured and relevant to the file's purpose. The naming is clear and the typing is explicit, which enhances code readability and maintainability.


1-91: Well-structured configuration options for the pop-out clock feature.

This file effectively defines a comprehensive set of configuration options for the pop-out clock feature, aligning well with the PR's objective of implementing a proof of concept for a Video Picture-in-Picture (PiP) feature. The options cover various aspects of the timer's appearance and behavior, providing flexibility for future customization.

Key strengths:

  1. Logical organization of options into sections.
  2. Clear and descriptive naming of options.
  3. Comprehensive coverage of styling and positioning options.

The minor suggestions provided in the review will further enhance the clarity and consistency of the options. Overall, this file forms a solid foundation for the pop-out clock functionality.

apps/client/src/AppRouter.tsx (3)

19-19: LGTM: New import for timer-related types.

The addition of this import is appropriate and necessary for the new PopOutTimer component. It's good to see that shared types are being used, which helps maintain consistency across the application.


27-27: LGTM: Lazy loading of PopOutTimer component.

The lazy loading of the PopOutTimer component is implemented correctly and consistently with other components in this file. This approach is beneficial for code splitting and can improve the application's initial load time.


Line range hint 1-156: Summary: PopOutTimer implementation is a good start for the proof of concept.

The changes introduce the PopOutTimer component to the application router, which aligns with the PR's objective of creating a proof of concept for a pop-out clock feature. The implementation follows the existing patterns in the file, such as lazy loading and proper placement within the React.Suspense wrapper.

Next steps to consider:

  1. Refactor the hardcoded values in the PopOutTimer props to use dynamic data from the application's state.
  2. Implement proper error handling and loading states for the lazy-loaded component.
  3. Add unit tests for the new component and its integration with the router.
  4. Update the application's documentation to include information about this new feature.

Overall, this is a solid foundation for the pop-out clock feature. Great job on getting this proof of concept implemented!

apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx (2)

112-122: Review the necessity of playing the video after setting the source

After setting videoElement.srcObject or videoElement.src, the call to videoElement.play() should be reviewed. Multiple calls to play() might result in warnings or errors in some browsers, especially if the video is already playing.


70-81: ⚠️ Potential issue

Parameter text in drawFrame should reflect the timer display

In the drawFrame function, the text parameter receives title, which is always an empty string. To display the timer correctly, ensure that text receives the display value.

Apply this diff to fix the parameter:

- let title = "";
...
- changeVideo(color, title, context, canvas, videoElement);
+ changeVideo(color, display, context, canvas, videoElement);

Likely invalid or redundant comment.

Comment on lines +22 to +53
{ section: 'View style override' },
{
id: 'key',
title: 'Key Colour',
description: 'Background colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: 'fffff (default)',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Colour of text background in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
placeholder: 'Arial Black (default)',
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix inconsistency in text color placeholder.

The "View style override" section is well-structured, providing comprehensive customization options. However, there's an inconsistency in the text color placeholder.

Apply this change to fix the inconsistency:

-    placeholder: 'fffff (default)',
+    placeholder: 'ffffff (default)',

This change ensures that the placeholder represents a valid 6-digit hexadecimal color code, consistent with the other color options.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ section: 'View style override' },
{
id: 'key',
title: 'Key Colour',
description: 'Background colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: 'fffff (default)',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Colour of text background in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
placeholder: 'Arial Black (default)',
},
{ section: 'View style override' },
{
id: 'key',
title: 'Key Colour',
description: 'Background colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: 'ffffff (default)',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Colour of text background in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
placeholder: 'Arial Black (default)',
},

Comment on lines +78 to +101
<PopOutTimer
isMirrored={false}
time={{
addedTime: 0,
current: null,
duration: null,
elapsed: null,
expectedFinish: null,
finishedAt: null,
phase: TimerPhase.Default,
playback: Playback.Play,
secondaryTimer: null,
startedAt: null,
clock: 0,
timerType: TimerType.CountDown
}}
viewSettings={{
dangerColor: '',
endMessage: '',
freezeEnd: false,
normalColor: '',
overrideStyles: false,
warningColor: ''
}} />
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider refactoring hardcoded values in PopOutTimer props.

While the placement of the PopOutTimer component is correct, there are a few points to consider:

  1. The time prop contains many hardcoded values. This could lead to maintenance issues and might not reflect the actual state of the application.
  2. The viewSettings prop has empty strings for colors and messages.

Given that this is a proof of concept, these issues are understandable. However, for the next iteration, consider:

  • Connecting the time prop to your application's state management system (e.g., Redux, Context API) to reflect real-time values.
  • Implementing a configuration file or environment variables for the viewSettings to make them easily customizable.
  • Adding PropTypes or TypeScript interfaces to define the expected prop types and structures.

Would you like assistance in setting up a more dynamic prop structure for the PopOutTimer component?

Comment on lines +38 to +48
useEffect(() => {
const canvas = canvasRef.current;
const videoElement = videoRef.current;
if (canvas && videoElement) {
const context = canvas.getContext('2d');
if (context) {
changeVideo(color, title, context, canvas, videoElement);
}
setReady(true);
}
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Variables color and title are not updated; consider removing or updating them

In the useEffect hook, changeVideo is called with color and title, but these variables are not updated anywhere in the component. As a result, the video might not display the intended content.

If color and title are meant to change based on certain conditions or props, ensure they are updated accordingly. Otherwise, consider removing them if they are not needed.

Comment on lines +125 to +136
useEffect(() => {
if (ready && canvasRef.current && videoRef.current) {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
let i = 0;
const interval = setInterval(() => {
changeVideo("green", display, context!, canvas, videoRef.current!);
i++;
}, 1000);
return () => clearInterval(interval); // Clean up the interval on component unmount
}
}, [ready]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider updating the useEffect to handle changes in time

The display value is derived from time, and if time changes, the interval may not reflect the updated timer. Consider updating the useEffect to handle changes in time to ensure the timer stays accurate.

Update the dependency array and adjust the effect:

- }, [ready, display]);
+ }, [ready, time]);

Committable suggestion was skipped due to low confidence.


⚠️ Potential issue

Include dependencies in the useEffect hook to prevent stale values

The useEffect hook depends on display, which is derived from time and may change over time. Currently, the dependency array only includes [ready], which could lead to the interval using stale values of display.

Consider adding display or its dependencies to the dependency array to ensure the interval reflects the latest timer values.

Apply this diff to fix the dependency array:

- }, [ready]);
+ }, [ready, display]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (ready && canvasRef.current && videoRef.current) {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
let i = 0;
const interval = setInterval(() => {
changeVideo("green", display, context!, canvas, videoRef.current!);
i++;
}, 1000);
return () => clearInterval(interval); // Clean up the interval on component unmount
}
}, [ready]);
useEffect(() => {
if (ready && canvasRef.current && videoRef.current) {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
let i = 0;
const interval = setInterval(() => {
changeVideo("green", display, context!, canvas, videoRef.current!);
i++;
}, 1000);
return () => clearInterval(interval); // Clean up the interval on component unmount
}
}, [ready, display]);

Comment on lines +83 to +103
const createVideoBlob = (canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, callback: (url: string) => void) => {
const stream = canvas.captureStream(30);
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
const chunks: BlobPart[] = [];

mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};

mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
callback(URL.createObjectURL(blob));
};

mediaRecorder.start();
setTimeout(() => {
mediaRecorder.stop();
}, 100);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize video generation by using a continuous stream instead of creating blobs repeatedly

Currently, createVideoBlob is called every second, creating a new video blob each time. This approach is inefficient and may cause performance issues.

Consider using the canvas stream directly as the source for the video element without creating new blobs. This can be achieved by assigning the canvas stream to the video element's srcObject.

Apply this diff to refactor the video handling:

- createVideoBlob(canvas, context, (newVideoSource) => {
-   if (videoSource) {
-     URL.revokeObjectURL(videoSource);
-   }
-   setVideoSource(newVideoSource);
-   videoElement.src = newVideoSource;
-   videoElement.play().catch((error) => {
-     console.error("Error playing video:", error);
-   });
- });
+ const stream = canvas.captureStream(30);
+ videoElement.srcObject = stream;
+ videoElement.play().catch((error) => {
+   console.error("Error playing video:", error);
+ });

Committable suggestion was skipped due to low confidence.


💡 Codebase verification

Proper Cleanup of Media Resources is Not Fully Implemented

  • In apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx, the URL.createObjectURL is used to create a blob URL but is never revoked using URL.revokeObjectURL, which can lead to memory leaks.
  • Additionally, ensure that the media stream obtained from canvas.captureStream is explicitly stopped after usage to release all associated resources.
🔗 Analysis chain

Ensure proper cleanup of media resources to prevent memory leaks

When creating media streams and blobs, it's crucial to properly clean up resources to avoid memory leaks. Ensure that any streams, blobs, or object URLs are correctly released when no longer needed.

Run the following script to check for proper cleanup of media resources:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for any unreleased media resources in the codebase.

# Expectation: All created media streams and object URLs should be properly revoked or released.

rg --type js 'URL\.createObjectURL|captureStream' -A 5

Length of output: 56


Script:

#!/bin/bash
# Description: Search for any unreleased media resources in JavaScript and TypeScript files.

# Expectation: All created media streams and object URLs should be properly revoked or released.

rg --type js --type ts --type jsx --type tsx 'URL\.createObjectURL|captureStream' -A 5

Length of output: 116


Script:

#!/bin/bash
# Description: Search for any unreleased media resources in JavaScript and TypeScript files, including JSX and TSX.

# Define custom types for jsx and tsx
rg --type-add 'jsx:*.jsx' --type-add 'tsx:*.tsx' 'URL\.createObjectURL|captureStream' -t js -t ts -t jsx -t tsx -A 5

Length of output: 1906

Comment on lines +34 to +36
let color = "#000000";
let title = "";
let clicked = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid using let for variables that persist across renders; use useState or useRef instead

The variables color, title, and clicked are declared with let and are re-initialized on every render. This means they won't retain any changes between renders, which may lead to unexpected behavior.

To maintain their values across renders, consider using useState or useRef. Additionally, if title and clicked are not being updated or used elsewhere in the component, they might be unnecessary.

Apply this diff to fix the issue:

- let color = "#000000";
- let title = "";
- let clicked = false;
+ const colorRef = useRef("#000000");
+ const titleRef = useRef("");
+ const clickedRef = useRef(false);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let color = "#000000";
let title = "";
let clicked = false;
const colorRef = useRef("#000000");
const titleRef = useRef("");
const clickedRef = useRef(false);
```
This suggestion replaces the original `let` declarations with `useRef` hooks, which will allow the values to persist across renders. The suggestion is complete and can be directly applied to replace the original code snippet.
Note: To use `useRef`, make sure to import it from 'react' at the top of the file if it's not already imported:
```typescript
import { useRef } from 'react';

@cpvalente cpvalente changed the base branch from master to feat-popout October 18, 2024 18:30
@cpvalente
Copy link
Owner

Thank you @Haavard15 , we will plug in the data from here

@cpvalente cpvalente merged commit f2bb803 into cpvalente:feat-popout Oct 18, 2024
2 of 4 checks passed
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.

2 participants