Compare commits

...

99 Commits

Author SHA1 Message Date
lebaudantoine c5838ae8f1 wip try 2024-09-25 19:55:35 +02:00
lebaudantoine bece79f47b ♻️(frontend) increase silent login attempts to every 5 minutes
Maximize logged-in users. This ensures that users remain authenticated
more consistently across sessions without manual intervention.
2024-09-25 14:45:09 +02:00
lebaudantoine a499331960 🐛(frontend) prevent silent login during active calls
React Query refetches stale data on window focus, which triggers
silent login attempts for logged-out users.

If user data becomes stale (after 1 hour), silent login redirects
the user to the auth domain, potentially disconnecting them during calls.

To prevent this, user data is now considered non-stale
during active sessions.

If the user logs in via another tab, a manual page
reload will be required to refresh their session.
2024-09-25 14:45:09 +02:00
lebaudantoine d50d167d0a 🐛(helm) fix certificates configuration
Newly introduced ingress for PostHog were misconfigured, oopsie.
Here is the fix.
2024-09-25 11:58:34 +02:00
lebaudantoine e4c7bc0826 ♻️(helm) separate PostHog ingress
Based on feedback from @rouja, I've updated the Helm configuration for PostHog
to use separate ingress resources for each service. Although the documentation
suggests sharing the same ingress, the services have different externalName
values, which conflicts with the use of a vhost in the ingress annotations.
This change ensures proper service redirection by aligning each service with
its own ingress.
2024-09-25 11:40:44 +02:00
lebaudantoine b5244a5ec0 🔧(helm) configure support and analytics
Declare the expected support and analytics env variables
expected by the frontend for each environment. To avoid consuming
too much credits from our PostHog free tier plan.
2024-09-25 11:40:44 +02:00
lebaudantoine 8cc2cc83c6 (frontend) initialize application from remote configs
Query API to get the App's configs.
Replacement for env variables, thus with a single image,
we can dynamically update frontend's behavior.

Code inspired by Marsha. Not sure having a single component
returning null is a good idea, to be discussed.
2024-09-25 11:40:44 +02:00
lebaudantoine e591d09b00 (backend) add endpoint for frontend's configuration
Inspired by Joanie.

In Frontend context, env variables are only available at build time,
not runtime. This is one of the easiest way to pass frontend dynamic
configurations while running.

This commit only exposes the view already existing.
2024-09-25 11:40:44 +02:00
Jacques ROUSSEL eeb4dae12d 💚(ci) fix argocd job which handles sync
Fixed by @rouja. ArgoCD should be more robust than ever, while
syncing with our code.
2024-09-25 11:40:44 +02:00
lebaudantoine 271389d459 🚨(helm) fix linter errors
Initial set-up errors. Fixed them. Removed unused Preprod environment,
as secrets are not configured yet, it raises an error.
2024-09-25 11:40:44 +02:00
Jacques ROUSSEL fe6eefa1f0 👷(ci) lint helmfile
Introduced by @rouja. Added a new linter to ensure helm and yaml
files can be properly parsed into templates.
ArgoCD can not break anymore.
2024-09-25 11:40:44 +02:00
lebaudantoine a276517278 📈(frontend) setup a reverse proxy for analytics
Proxy analytics requests through our backend to minimize
ad-blockers impact. I configured the Helm Charts following
PostHog official documentation.
2024-09-25 11:40:44 +02:00
renovate[bot] 9e9b9015f4 ⬆️(dependencies) update js dependencies 2024-09-24 10:49:28 +02:00
lebaudantoine 3f0378aada ♻️(frontend) remove Tchap support
Tchap is no longer relevant due to the integration of Crisp.
The Tchap channel will be deleted.
2024-09-24 10:49:14 +02:00
Jacques ROUSSEL 90c88a8bd3 🔒️(helm) change domainon production
Add ingress in order to migrate from meet.numerique.gouv.fr to
visio.numerique.gouv.fr
2024-09-23 20:20:56 +02:00
lebaudantoine 1103902c12 🔖(minor) bump release to 0.1.6
Release to 0.1.6
2024-09-23 20:10:16 +02:00
lebaudantoine 812016d09c (frontend) add user support feature using Crisp
Implement a chat feature that allows users to communicate directly with an
engineer for assistance with issues. We use Crisp for consistency, as it’s
already utilized in all other LaSuite products. Additionally, we need to
configure an environment variable in the frontend for better flexibility.

This is the initial implementation, and session handling will be refined
in future updates.
2024-09-23 20:06:29 +02:00
lebaudantoine cadc5c0034 (frontend) add crisp-sdk-web dependency
La Suite uses Crisp for Chat support.
Added their react sdk.
2024-09-23 20:06:29 +02:00
lebaudantoine b083d837f8 ♻️(frontend) encapsulate PostHog logic in dedicated functions
Decouple the code from the PostHog dependency by wrapping it in a custom hook
and several utility functions. This enhances code maintainability and separation
of concerns.
2024-09-23 18:58:38 +02:00
lebaudantoine db8445f4ab 🐛(backend) add missing Django settings
Fixed an issue where a setting cloned from Joanie was missing
in the  Django configuration. Although its value was provided, it was
not applied due to the missing reference in the settings file.
2024-09-23 18:12:44 +02:00
lebaudantoine 0e5c2be445 (frontend) replace login link by ProConnect button
Following their documentation, replace our generic login button,
by a more specific one. Asked by several users.
2024-09-23 17:31:07 +02:00
lebaudantoine 0131a65509 ✏️(frontend) fix minors typos in i18n
Found and fixed minor typos in our copywritting.
2024-09-23 16:34:11 +02:00
lebaudantoine fb3727e73e 👔(frontend) rename 'meet' to 'visio'
In alignment with French government guidelines,
ensuring no English terms slip through.
2024-09-23 16:34:11 +02:00
Jacques ROUSSEL 3391165e4b 🔒️(helm) change domain
Change the domain to visio-staging.beta.numerique.gouv.fr
2024-09-23 12:07:13 +02:00
Jacques ROUSSEL 0be94aa572 🔒️(helm) setup temporary redirect
Add a specific certificate to prepare redirect
2024-09-23 12:07:13 +02:00
lebaudantoine b309f91095 💡(frontend) highlight technical debt
We should use participant attributes, rather than metadata, to store hand
status (raised/lowered). However, the useParticipantInfo hook is currently
not suited for this, as it does not refresh when attributes change.
2024-09-22 20:02:33 +02:00
lebaudantoine c50a749293 ♻️(frontend) simplify raised hand notification
Listen to metadata changed events instead of messaging the whole
room. Less code, might be less clean, to be discussed.
2024-09-22 20:02:33 +02:00
lebaudantoine cd9e7c8a1f 🚸(frontend) inform user Blurring effects are experimental
Be transparent with users: this version lacks quality and is currently
limited to Chrome, which may lead to frustration.

In addition to providing information, we should implement a method to
collect user feedback and reactions.
2024-09-22 20:02:33 +02:00
lebaudantoine 9d0767ccfe ♻️(frontend) refactor useParticipants to streamline updatesOnlyOn logic
Simplified the logic to avoid redundancy. Removed unnecessary duplication of
eventGroups, as it's already exported from the core. My bad for the initial
complication — code is now cleaner.
2024-09-22 20:02:33 +02:00
lebaudantoine 021e52d9eb 💄(frontend) organize blurring options
Add visual container and a heading to indicate buttons are
blurring options.
2024-09-22 20:02:33 +02:00
lebaudantoine 607a5fc99d (frontend) introduce blurring effects for Chromium-based users
Implemented using MediaPipe, which is not supported on Firefox due to
limitations outlined in issue #38 of the track-processors-js repo.

We decided to release this first version exclusively for Chrome to quickly
offer a solution to users. Future iterations will address broader compatibility.

An informational message will be added to notify users that the feature is
experimental. For now, a text label will be used in place of an icon.
2024-09-22 20:02:33 +02:00
lebaudantoine 756be17cc7 (frontend) introduce a side panel for effects
It simply renders the video track if enabled. It's a basis
for building the 'blur your screen' feature.

More in the upcoming commits.
2024-09-22 20:02:33 +02:00
lebaudantoine 00fa7beebd ♻️(frontend) introduce a side panel
Refactor side panel into a reusable component to display any interactive
content like menus, messages, participant lists, or effects. Establish it
as a core feature of the videoconference tool.

Improve extensibility and better share responsibilities. The next step is to
refactor the chat to render inside the side panel.
2024-09-22 20:02:33 +02:00
lebaudantoine b9d13de591 (frontend) introduce 'effects' menu item
Increased all icons size, as they looked a bit small on screen.
Introduce a new component separator, to organize menu items
in section.

Work in progress, added the 'effects' item, which triggers no
action yet.
2024-09-22 20:02:33 +02:00
lebaudantoine 7896890ffc ♻️(frontend) refactor translation keys to use prefix
Code cleaning. Avoid duplication.
2024-09-22 20:02:33 +02:00
lebaudantoine a2043abb51 (frontend) install track-processors-js from LiveKit
Recommended dependency to process video tracks, to add a virtual
background, or a blurring effect. Created and maintained by the
core LiveKit's team.

Please note, few issues are known. track-processors-js relies on
MediaPipe (by Google), which is not supported on Firefox …

Quick-win to ship a first draft.
2024-09-22 20:02:33 +02:00
rouja 64bb1b3bb5 Merge pull request #161 from numerique-gouv/fix-secret-mep-issue
🔒️(helm) fix secret sync precedence
2024-09-20 15:04:29 +02:00
Jacques ROUSSEL a1a56402d1 🔒️(helm) fix secret sync precedence
When new secret is added to backend secret, it's not sync at the
beginning of argocd synchronisation and jobs are blocked. Theses new
annotations fix this issue.
2024-09-20 14:55:15 +02:00
lebaudantoine 582d3774a4 ⬆️(frontend) fix persistent choices nesting issue
Upgraded to livekit/components-react v2.5.4, fixing a bug in v2.5.3 that
caused repeated nesting of persistent user choices, exhausting the quota.
Thanks to Lukas for the quick fix.
2024-09-17 19:43:40 +02:00
lebaudantoine fab08cfaf8 🚑️(frontend) fix posthog user's ID
Quotes were misplaced, leading to the same ID being generated for every user.
Fixed the issue, which was a trivial error.
2024-09-17 18:26:53 +02:00
lebaudantoine 550c48f29e 📈(frontend) track room join events with room slug
Focus on join events as a core metric for user room participation.
Added properties to differentiate events by room slug.
2024-09-17 15:28:52 +02:00
lebaudantoine 0c64580abf 📈(frontend) track page view events in SPA
Implement page view tracking in the SPA following PostHog’s guidelines.
Manually capture page's location when it changes.
2024-09-17 15:28:52 +02:00
lebaudantoine 0ad9b7e233 📈(frontend) limit event tracking to stay within free tier limits
Optimize event tracking to be frugal and reduce event volume,
even though filtering can be done later if needed.
2024-09-17 15:28:52 +02:00
lebaudantoine b9bcc45cce 📈(frontend) add explicit data attributes for key event tracking
PostHog's autocapture is useful, but explicit tracking ensures cleaner, more
focused data. This initial pass targets high-impact actions; we'll iterate as
needed.
2024-09-17 15:28:52 +02:00
lebaudantoine bcd285e368 ♻️(frontend) refactor tooltip on screen share toggle
Reorganize tooltip to follow others toggle buttons.
2024-09-17 15:28:52 +02:00
lebaudantoine c0ad98eb34 ♻️(frontend) refactor keyprefix in translation
Factorized translations' prefix.
2024-09-17 15:28:52 +02:00
lebaudantoine 178278235e (frontend) login and logout from Posthog
Following Posthog documentation, login and logout users directly
from the client. Note: Logout function should be encapsulate in
a proper function.
2024-09-17 15:28:52 +02:00
lebaudantoine 11664956c7 (frontend) initialize posthog
Initialize Posthog script with our project's keys.
This key is public, it's not a secret.

Our data is hosted in Europe, followed Tchap integration,
which was reviewed by the ANSI.
2024-09-17 15:28:52 +02:00
lebaudantoine f020979188 (frontend) add posthog dependency
Posthog is a product analysis tool.
Installed the js SDK to track product usage.
2024-09-17 15:28:52 +02:00
renovate[bot] da5c8d6773 ⬆️(dependencies) update python dependencies 2024-09-16 18:08:35 +02:00
lebaudantoine 31051cd6c4 🚸(frontend) improve share screen UX
Enhanced the share screen button by adding a tooltip and improving contrast for
better accessibility. Created a temporary icon by combining two from Remix, but
it’s bulky and will need refactoring soon.
2024-09-16 18:02:53 +02:00
lebaudantoine c51058e6ac ✏️(frontend) fix a minor typo
Minor typo copied/pasted from LiveKit sources. Fixed it.
2024-09-16 18:02:53 +02:00
lebaudantoine 3ba913bb21 ♻️(frontend) use keyprefix args
My bad, avoid repeating the same prefix.
2024-09-16 18:02:53 +02:00
lebaudantoine 95116f70e8 🚸(frontend) enhance quit button UX
Improved handling of the disconnect call by properly managing the returned
promise. Simplified the approach by skipping the useDisconnectButton hook.
Updated the button to display only an icon for a cleaner interface.
2024-09-16 18:02:53 +02:00
lebaudantoine 5b282b62e7 🩹(frontend) dismiss notification toasts on room disconnect
Ensure notification toasts are cleared when a participant disconnects,
preventing stale notifications from showing if the user quickly rejoins.
This resolves issues with duplicate or outdated notifications appearing.
2024-09-16 18:02:53 +02:00
lebaudantoine 5e74fce6e2 🚸(frontend) simplify audio and video select inputs
Based on @manuhabitela's works.
Align UX with common tools as Gmeet or Jitsi.
Enhanced accessibility.
2024-09-16 18:02:53 +02:00
lebaudantoine 96b279b350 🚨(frontend) fix eslint errors
Recent dependencies' updates triggered new
eslint errors. Fixed them.
2024-09-15 17:57:21 +02:00
lebaudantoine fc4ad562ae 📌(frontend) pin eslint dependency
Conflicts with eslint-plugin-react-hooks dependency,
which doesn't support the latest v9 of eslint.
2024-09-15 17:57:21 +02:00
renovate[bot] 480a8df31a ⬆️(dependencies) update js dependencies 2024-09-15 17:57:21 +02:00
lebaudantoine f76e7f0e51 🔖(minor) bump release to 0.1.5
Minor release pre-hackathon.
2024-09-12 01:34:44 +02:00
lebaudantoine b9ffbd179c 🌐(frontend) enhance few English traductions
Found few issues, fixed them.
2024-09-12 01:10:19 +02:00
lebaudantoine a3ef1d4a26 🩹(frontend) enable React Query for token management
Previously, I prevented clients from fetching new tokens when data is stale.
It seems totally wrong. Enabling data refresh addresses potential issues
with random user disconnections.

Please note the data refresh when stale was only disabled for people
who created the room.
2024-09-12 00:55:13 +02:00
lebaudantoine d11bcc5de9 🩹(frontend) remove undesired console.log
Oupsy, I forgot to remove this one. My bad.
2024-09-12 00:44:35 +02:00
lebaudantoine 412914cf01 💄(frontend) force a minimum left column width
While in focus, left tiles should be large enough to render
long name and still have a big enough size to get who is in the
tile. Temporary, but functional.
2024-09-12 00:44:35 +02:00
lebaudantoine d511aedd39 🚸(frontend) draft sound design on notifications
More a brainsto idea inspired from Gmeet than a polished feature.
Try adding sound notification to add a new dimension on the user
experience.
2024-09-12 00:44:35 +02:00
lebaudantoine 7edf7d194b 🚸(frontend) disable toast notifications on mobile
Due to responsiveness issues and awkward display on mobile screens.

Note: Chrome inspector's responsive mode inaccurately triggers mobile
behavior. It uses a temporary function imported from LiveKit.
2024-09-12 00:44:35 +02:00
lebaudantoine 6324608a4a 🩹(frontend) improve join notification handling
Notifications now close if the designated user leaves the room before
the toaster duration ends. This prevents bugs and ensures cleaner UX.
2024-09-12 00:44:35 +02:00
lebaudantoine a402c2f46f (frontend) notify user when hands are raised
Using toast, first draft of a toast notification inspired by Gmeet.
Users can open the waiting list to see all the raised hands.
2024-09-12 00:44:35 +02:00
lebaudantoine e21858febe ♻️(frontend) extract duplicated logic into reusable hook
This hook manages interactions with the right side widget,
improving maintainability.
2024-09-12 00:44:35 +02:00
lebaudantoine 3d615fa582 (frontend) introduce in-app toast notification
Toast-related components are reusable. They follow React Aria recommendations.

The UI is heavily inspired by GMeet, we should iterate on it.
Why toast? It gives additional information and quick actions when an event
occurs in the room.

The ToastRegion design should be improved to be more extensible, to move away
from poor practices with conditional rendering.

Added initial toast display when users join, with room for further improvement.
Please read the fixme items.
2024-09-12 00:44:35 +02:00
lebaudantoine 4293444d3e 🚨(frontend) clean unused imports
Found unused imports that were breaking the build. Fixed them.
2024-09-12 00:44:35 +02:00
lebaudantoine a55a4d5b5d ♻️(frontend) disable participant's tile metadata
Inspired by the props disableSpeakingIndicator, used the same
logic for all metadata displayed on hover on a participant's tile.

Why? I'll need to render the joining participant tile without any
metadata, so the participant see who is joining the room.
2024-09-12 00:44:35 +02:00
lebaudantoine dd9a87a33b (frontend) install react-toast in beta
Absolutely needed some toasts to handle in app notifications,
or feedbacks. React Aria is working on it, and has released a
first beta. No component is available, only hooks.
2024-09-12 00:44:35 +02:00
lebaudantoine 24242ef01a 💄(frontend) introduce a new button's variant
'text' is reserved for discreet buttons. Inspired by Gmeet,
styles need to be refined. WIP, but mergeable as it is.
2024-09-12 00:44:35 +02:00
lebaudantoine b537cdfd93 🚸(frontend) align later meeting modal
Duplicated sources, bad practice, should be a unique and reusable
component. Debt, needs to be refactored.
2024-09-12 00:44:35 +02:00
lebaudantoine 80cc7c723f 🚸(frontend) enhance accessibility of 'copy link' button
Added user feedback for actions with explicit text indicating
URL will be copied to clipboard. Modeled after Jitsi's button
behavior for clarity and consistency in user experience.
2024-09-12 00:44:35 +02:00
lebaudantoine e9210213b1 💄(frontend) enhance invitation modal position
Based on User's feedback.

Having the dialog position visually in the participant tile is
confusing for the user.

Position the dialog to be explicitly outside of a participant
tile container.
2024-09-12 00:44:35 +02:00
lebaudantoine 634f1924be 🚸(frontend) enhance muted mic indicator
Users' feedbacks. Design is inspired from Whereby, a webinars
product. I am not a huge fan of having the indicator on the
left of the participant's name. It causes a shift of the name
when the participant unmute herself.

This should be discussed with the UX designer.
2024-09-12 00:44:35 +02:00
lebaudantoine 4e175a8361 🚸(frontend) highlight raised hands in participant's tile
Users's feedbacks. Raised hands in participants' tiles were not
highlighted enough.

It was discussed adding a colorful border around the participant's
tile (idea inspired by Jitsi). Nonetheless, it conflicts with the
current blue border, indicating when a participant is speaking.

Until this blue border is present, it's not a great idea having a
second border indicating a new level of informations.

@sampacoud requested a yellow-ish/colorful indicator of when a
participant raised her hand, inspired by Jitsi. However, I think
yellow color should be reserved for warning or dangerous action.

I chose to use a white background around the participant's name,
to reinforce the visual indication a participant has her hand
raised, while still being discreet. It's inspired by Gmeet.
2024-09-12 00:44:35 +02:00
lebaudantoine c5ce32ef79 🚸(frontend) enhance mic indicator in participant list
The Gmeet-inspired indicator was misleading for users,
at least those not used to GSuite products.

They didn't understand how to mute a participant.

Plus, having the button disabled for the local participant,
was creating confusion. To simplify the UX, have all
the buttons enabled is simpler to understand.

Empirical observations with a few number of users, should be
enhance and challenge in the long run.
2024-09-12 00:44:35 +02:00
lebaudantoine aaf6b03a25 🚸(frontend) reduce delay for instant tooltip
Instant tooltips weren't really instant. Reduced the delay
for instant tooltip, it was empirically tuned, can be enhanced.
2024-09-12 00:44:35 +02:00
lebaudantoine 66bc739411 🩹(frontend) fix cursor on disabled button
Having a pointer cursor on a disabled button was misleading
fixed it ! Minor issue.
2024-09-12 00:44:35 +02:00
lebaudantoine 1b48fa256e ♻️(frontend) get rid of the ListItemActionButton
Use the primitive Button component, avoid maintaining two button
components.
2024-09-12 00:44:35 +02:00
lebaudantoine 053c4a40e9 🩹(backend) fix identity hash randomness
'hash' built-in function is randomly seed by Python process.
In staging or production, our backend runs over 3 pods, thus 3
Python processes. For a given identity, it was not prompting
the same hash across all pods.

Why 'hash' is randomly seed? For security reasons, there was
a vulnerability disclosure exploiting key collision. Since Python 3.2,
'hash' is by default randomly seed.

Fixed it! Thx @jonathanperret for your help.
2024-09-04 14:15:38 +02:00
lebaudantoine 53d732d802 💫(frontend) wave icon when a participant raises her hand
Minor enhancement, add some CSS animation to wave a newly
raised hand. Inspired by GMeet.
2024-09-03 16:01:49 +02:00
lebaudantoine 26ca81db40 🩹(frontend) fix color contrast when toggling a control
Copied legacy style from the screen share control.
Still imperfect, but at least it's visually more "comfy".
2024-09-03 16:01:49 +02:00
lebaudantoine e6e6a3bde7 💩(frontend) indicate when a participant raises her hand
Height was varying when muting/unmuting the mic, so I added a minimum height to
avoid layout shift.

Proof of concept for the future UX: a raised hand would be placed on
the left side of the participant's name.

Having the mic indicator and raised hand side by side is temporary and totally
undesired (it feels super weird). I'll refactor the whole UX of the participant
tile in a dedicated PR.
2024-09-03 16:01:49 +02:00
lebaudantoine 26fdaac589 ♻️(frontend) extract raised hand logic in a reusable hook
Encapsulate code responsible for toggling hand,
and determining the hand state based on participant metadata.

This gonna be reused across the codebase.
2024-09-03 16:01:49 +02:00
lebaudantoine 1fd1cb71ba 📱(frontend) enable Y-scroll on participants menu
Responsiveness issue. Fixed it, to allow user scrolling the
participants list.
2024-09-03 16:01:49 +02:00
lebaudantoine c8023573c6 (frontend) allow lowering all hands at once
Inspired from Gmeet. Add a button to lower all hands at once.
It should be quite useful for room moderator/admin.

The UX might feel strange, having this action button at the top of
the list, but I could not figure out a better layout. In terms of
UX I feel this is the best we can provide. However, the UI should
definitely be improved.
2024-09-03 16:01:49 +02:00
lebaudantoine 584be7e65b (frontend) add raised hands waiting room
Show the raised hand waiting list, allow any participant to
lower other participants' hands.

Inspired from Gmeet. I found it quite clear to have a dedicated
waiting list for raised hands.
2024-09-03 16:01:49 +02:00
lebaudantoine 59ec88e84a ♻️(frontend) extract action icon button in a component
Will need it for lower hand action.
Actually, this piece of code should be enhanced. It'll act
as a corner stone of all actions dispatched in the interface.

I'll continue refactoring it later on.
2024-09-03 16:01:49 +02:00
lebaudantoine 1b2e0ad431 🚸(frontend) introduce sections in participants menu
Why? This layout is extensible. This menu will have two sections,
to separate in-room participants from the waiting room ones.

It's copied from Gmeet User Experience.

In-room participants will be divided in sub-lists based on their
states (ex: hand raised, …). This User Experience is extensible
if in the future with support subroom in a room or whatever.
2024-09-03 16:01:49 +02:00
lebaudantoine 20464a2845 (frontend) introduce raise hand control
Inspired by Magnify. Rely on participant metadata to determine,
if the local user has raised its hand.

There is a todo item in LiveKit code, useLocalParticipant is not
subscribed to metadata updates events.

The contrast of the toggled button in legacy Style is poor,
it needs to be urgently improved. User can barely distinguish if
their hands are raised.
2024-09-03 16:01:49 +02:00
renovate[bot] ece6284de2 ⬆️(dependencies) update python dependencies 2024-09-02 11:31:49 +02:00
lebaudantoine 82e994e5b1 (frontend) add active speaker indicator to audio tab
Aligned layout and features with Gmeet. The layout is functional,
though the code needs cleanup.

Will open an issue for V3 enhancements.
2024-08-30 16:32:41 +02:00
lebaudantoine 85aa7a7251 ♻️(frontend) wrap text for long selected value
These changes should be discussed. Needed for the audio selects,
that render super long text values.

By default, centered texts are sometime hard to read.
2024-08-30 16:32:41 +02:00
lebaudantoine c4ececd03a ♻️(frontend) collapse tabs when modal is not wide
Inspired by Gmeet UX. Styling of the icon-only tabs should be
enhanced.

Heavily rely on JS, which is quite sad for this simple
responsive layout.
2024-08-30 16:32:41 +02:00
lebaudantoine 57bba04cf3 (frontend) pass ref to the dialog inner overlay
Needed a ref to determine the size of the opened overlay.
2024-08-30 16:32:41 +02:00
107 changed files with 5395 additions and 3569 deletions
+22
View File
@@ -0,0 +1,22 @@
name: Helmfile lint
run-name: Helmfile lint
on:
pull_request:
branches:
- 'main'
jobs:
helmfile-lint:
runs-on: ubuntu-latest
container:
image: ghcr.io/helmfile/helmfile:latest
steps:
-
uses: numerique-gouv/action-helmfile-lint@main
with:
app-id: ${{ secrets.APP_ID }}
age-key: ${{ secrets.SOPS_PRIVATE }}
private-key: ${{ secrets.PRIVATE_KEY }}
helmfile-src: "src/helm"
repositories: "meet,secrets"
+1
View File
@@ -8,3 +8,4 @@ creation_rules:
- age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa #marie
- age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw #argocd
- age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9 #manuuu
- age1hm2hsfgjezpsc3k0y5w5feq9t8vl3seq04qjhgt6ztd6403wfvpsgxu09m # github-repo
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -e
HELMFILE=src/helm/helmfile.yaml
environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=0}' "$HELMFILE" | grep -E '^[[:space:]]{2}[a-zA-Z]+' | sed 's/^[[:space:]]*//;s/:.*//')
for env in $environments; do
echo "################### $env lint ###################"
helmfile -e $env -f src/helm/helmfile.yaml lint || exit 1
echo -e "\n"
done
+3 -1
View File
@@ -13,7 +13,9 @@
"enabled": false,
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": []
"matchPackageNames": [
"eslint"
]
}
]
}
+1 -1
Submodule secrets updated: f5fbc16e6e...8ef9f4513a
+2 -1
View File
@@ -5,7 +5,7 @@ from django.urls import include, path
from rest_framework.routers import DefaultRouter
from core.api import viewsets
from core.api import get_frontend_configuration, viewsets
from core.authentication.urls import urlpatterns as oidc_urls
# - Main endpoints
@@ -23,6 +23,7 @@ urlpatterns = [
[
*router.urls,
*oidc_urls,
path("config/", get_frontend_configuration, name="config"),
]
),
),
+6 -1
View File
@@ -4,6 +4,7 @@ Utils functions used in the core app
# ruff: noqa:S311
import hashlib
import json
import random
from typing import Optional
@@ -24,7 +25,11 @@ def generate_color(identity: str) -> str:
range and ensure predictability.
"""
random.seed(hash(identity))
# ruff: noqa:S324
identity_hash = hashlib.sha1(identity.encode("utf-8"))
# Keep only hash's last 16 bits, collisions are not a concern
seed = int(identity_hash.hexdigest(), 16) & 0xFFFF
random.seed(seed)
hue = random.randint(0, 360)
saturation = random.randint(50, 75)
lightness = random.randint(25, 60)
+14
View File
@@ -251,6 +251,19 @@ class Base(Configuration):
"REDOC_DIST": "SIDECAR",
}
# Frontend
FRONTEND_CONFIGURATION = {
"analytics": values.DictValue(
{}, environ_name="FRONTEND_ANALYTICS", environ_prefix=None
),
"support": values.DictValue(
{}, environ_name="FRONTEND_SUPPORT", environ_prefix=None
),
"silence_livekit_debug_logs": values.BooleanValue(
False, environ_name="FRONTEND_SILENCE_LIVEKIT_DEBUG", environ_prefix=None
),
}
# Mail
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
EMAIL_HOST = values.Value(None)
@@ -258,6 +271,7 @@ class Base(Configuration):
EMAIL_HOST_PASSWORD = values.Value(None)
EMAIL_PORT = values.PositiveIntegerValue(None)
EMAIL_USE_TLS = values.BooleanValue(False)
EMAIL_USE_SSL = values.BooleanValue(False)
EMAIL_FROM = values.Value("from@example.com")
AUTH_USER_MODEL = "core.User"
+13 -13
View File
@@ -2,12 +2,12 @@
# Meet package
#
[build-system]
requires = ["setuptools"]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.4"
version = "0.1.6"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.35.5",
"boto3==1.35.19",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
@@ -36,11 +36,11 @@ dependencies = [
"django-redis==5.4.0",
"django-storages[s3]==1.14.4",
"django-timezone-field>=5.1",
"django==5.1",
"django==5.1.1",
"djangorestframework==3.15.2",
"drf_spectacular==0.27.2",
"dockerflow==2024.4.2",
"easy_thumbnails==2.9",
"easy_thumbnails==2.10",
"factory_boy==3.3.1",
"freezegun==1.5.1",
"gunicorn==23.0.0",
@@ -48,11 +48,11 @@ dependencies = [
"june-analytics-python==2.3.0",
"markdown==3.7",
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.2.1",
"psycopg[binary]==3.2.2",
"PyJWT==2.9.0",
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.13.0",
"sentry-sdk==2.14.0",
"url-normalize==1.4.3",
"WeasyPrint>=60.2",
"whitenoise==6.7.0",
@@ -71,18 +71,18 @@ dev = [
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2024.7.1",
"ipdb==0.13.13",
"ipython==8.26.0",
"ipython==8.27.0",
"pyfakefs==5.6.0",
"pylint-django==2.5.5",
"pylint==3.2.6",
"pylint==3.2.7",
"pytest-cov==5.0.0",
"pytest-django==4.8.0",
"pytest==8.3.2",
"pytest-django==4.9.0",
"pytest==8.3.3",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.3",
"ruff==0.6.2",
"types-requests==2.32.0.20240712",
"ruff==0.6.5",
"types-requests==2.32.0.20240914",
]
[tool.setuptools]
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/play-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Meet</title>
<title>Visio</title>
</head>
<body>
<div id="root"></div>
+2462 -2768
View File
File diff suppressed because it is too large Load Diff
+32 -27
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.4",
"version": "0.1.6",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -13,43 +13,48 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@livekit/components-react": "2.3.3",
"@livekit/components-styles": "1.0.12",
"@pandacss/preset-panda": "0.41.0",
"@livekit/components-react": "2.6.0",
"@livekit/components-styles": "1.1.2",
"@livekit/track-processors": "0.3.2",
"@pandacss/preset-panda": "0.46.1",
"@react-aria/toast": "3.0.0-beta.15",
"@remixicon/react": "4.2.0",
"@tanstack/react-query": "5.49.2",
"@tanstack/react-query": "5.56.2",
"crisp-sdk-web": "1.0.25",
"hoofd": "1.7.1",
"i18next": "23.12.1",
"i18next": "23.15.1",
"i18next-browser-languagedetector": "8.0.0",
"i18next-parser": "9.0.0",
"i18next-parser": "9.0.2",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.3.1",
"react": "18.2.0",
"react-aria-components": "1.2.1",
"react-dom": "18.2.0",
"react-i18next": "14.1.3",
"valtio": "1.13.2",
"wouter": "3.3.0"
"livekit-client": "2.5.3",
"posthog-js": "1.164.1",
"react": "18.3.1",
"react-aria-components": "1.3.3",
"react-dom": "18.3.1",
"react-i18next": "15.0.2",
"use-sound": "4.0.3",
"valtio": "2.0.0",
"wouter": "3.3.5"
},
"devDependencies": {
"@pandacss/dev": "0.41.0",
"@tanstack/eslint-plugin-query": "5.49.1",
"@tanstack/react-query-devtools": "5.49.2",
"@types/node": "20.14.9",
"@types/react": "18.3.3",
"@pandacss/dev": "0.46.1",
"@tanstack/eslint-plugin-query": "5.57.1",
"@tanstack/react-query-devtools": "5.56.2",
"@types/node": "20.16.6",
"@types/react": "18.3.8",
"@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.13.1",
"@typescript-eslint/eslint-plugin": "8.7.0",
"@typescript-eslint/parser": "8.7.0",
"@vitejs/plugin-react": "4.3.1",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-jsx-a11y": "6.10.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-react-refresh": "0.4.7",
"postcss": "8.4.39",
"eslint-plugin-react-refresh": "0.4.12",
"postcss": "8.4.47",
"prettier": "3.3.3",
"typescript": "5.5.2",
"vite": "5.3.1",
"vite-tsconfig-paths": "4.3.2"
"typescript": "5.6.2",
"vite": "5.4.5",
"vite-tsconfig-paths": "5.0.1"
}
}
+11
View File
@@ -75,6 +75,17 @@ const config: Config = {
'50%': { height: '4px' },
'100%': { height: '8px' },
},
wave_hand: {
'0%': { transform: 'rotate(0deg)' },
'20%': { transform: 'rotate(-20deg)' },
'80%': { transform: 'rotate(20deg)' },
'100%': { transform: 'rotate(0)' },
},
pulse_mic: {
'0%': { color: 'primary', opacity: '1' },
'50%': { color: 'primary', opacity: '0.8' },
'100%': { color: 'primary', opacity: '1' },
},
},
tokens: defineTokens({
/* we take a few things from the panda preset but for now we clear out some stuff.
Binary file not shown.
+2 -5
View File
@@ -11,18 +11,15 @@ import { Layout } from './layout/Layout'
import { NotFoundScreen } from './components/NotFoundScreen'
import { routes } from './routes'
import './i18n/init'
import { silenceLiveKitLogs } from '@/utils/livekit.ts'
import { queryClient } from '@/api/queryClient'
import { AppInitialization } from '@/components/AppInitialization'
function App() {
const { i18n } = useTranslation()
useLang(i18n.language)
const isProduction = import.meta.env.PROD
silenceLiveKitLogs(isProduction)
return (
<QueryClientProvider client={queryClient}>
<AppInitialization />
<Suspense fallback={null}>
<I18nProvider locale={i18n.language}>
<Layout>
+1
View File
@@ -1,4 +1,5 @@
export const keys = {
user: 'user',
room: 'room',
config: 'config',
}
+26
View File
@@ -0,0 +1,26 @@
import { fetchApi } from './fetchApi'
import { keys } from './queryKeys'
import { useQuery } from '@tanstack/react-query'
export interface ApiConfig {
analytics?: {
id: string
host: string
}
support?: {
id: string
}
silence_livekit_debug_logs?: boolean
}
const fetchConfig = (): Promise<ApiConfig> => {
return fetchApi<ApiConfig>(`config/`)
}
export const useConfig = () => {
return useQuery({
queryKey: [keys.config],
queryFn: fetchConfig,
staleTime: Infinity,
})
}
@@ -0,0 +1,20 @@
import { silenceLiveKitLogs } from '@/utils/livekit'
import { useConfig } from '@/api/useConfig'
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
import { useSupport } from '@/features/support/hooks/useSupport'
export const AppInitialization = () => {
const { data } = useConfig()
const {
analytics = {},
support = {},
silence_livekit_debug_logs = false,
} = data || {}
useAnalytics(analytics)
useSupport(support)
silenceLiveKitLogs(silence_livekit_debug_logs)
return null
}
File diff suppressed because one or more lines are too long
@@ -27,12 +27,17 @@ export const SoundTester = () => {
return (
<>
<Button
invisible
onPress={() => {
audioRef?.current?.play()
setIsPlaying(true)
}}
size="sm"
isDisabled={isPlaying}
fullWidth
style={{
color: isPlaying ? 'var(--colors-primary)' : undefined,
}}
>
{isPlaying ? t('audio.speakers.ongoingTest') : t('audio.speakers.test')}
</Button>
@@ -0,0 +1,39 @@
import { useEffect } from 'react'
import { useLocation } from 'wouter'
import posthog from 'posthog-js'
import { ApiUser } from '@/features/auth/api/ApiUser'
export const startAnalyticsSession = (data: ApiUser) => {
if (posthog._isIdentified()) return
const { id, email } = data
posthog.identify(id, { email })
}
export const terminateAnalyticsSession = () => {
if (!posthog._isIdentified()) return
posthog.reset()
}
export type useAnalyticsProps = {
id?: string
host?: string
}
export const useAnalytics = ({ id, host }: useAnalyticsProps) => {
const [location] = useLocation()
useEffect(() => {
if (!id || !host) return
if (posthog.__loaded) return
posthog.init(id, {
api_host: host,
person_profiles: 'always',
})
}, [id, host])
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
useEffect(() => {
posthog.capture('$pageview')
}, [location])
return null
}
@@ -20,7 +20,7 @@ export const fetchUser = (): Promise<ApiUser | false> => {
// make sure to not resolve the promise while trying to silent login
// so that consumers of fetchUser don't think the work already ended
if (canAttemptSilentLogin()) {
attemptSilentLogin(3600)
attemptSilentLogin(300)
} else {
resolve(false)
}
+12 -2
View File
@@ -2,9 +2,12 @@ import { useQuery } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys'
import { fetchUser } from './fetchUser'
import { type ApiUser } from './ApiUser'
import { useEffect } from 'react'
import { startAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
import { initializeSupportSession } from '@/features/support/hooks/useSupport'
/**
* returns info about currently logged in user
* returns info about currently logged-in user
*
* `isLoggedIn` is undefined while query is loading and true/false when it's done
*/
@@ -12,9 +15,16 @@ export const useUser = () => {
const query = useQuery({
queryKey: [keys.user],
queryFn: fetchUser,
staleTime: 1000 * 60 * 60, // 1 hour
staleTime: Infinity,
})
useEffect(() => {
if (query?.data) {
startAnalyticsSession(query.data)
initializeSupportSession(query.data)
}
}, [query.data])
const isLoggedIn =
query.status === 'success' ? query.data !== false : undefined
const isLoggedOut = isLoggedIn === false
@@ -23,7 +23,7 @@ export const JoinMeetingDialog = () => {
name="roomId"
label={t('joinInputLabel')}
description={t('joinInputExample', {
example: 'https://meet.numerique.gouv.fr/azer-tyu-qsdf',
example: 'https://visio.numerique.gouv.fr/azer-tyu-qsdf',
})}
validate={(value) => {
return !isRoomValid(value.trim()) ? (
@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { Div, Button, Dialog, Input, type DialogProps, P } from '@/primitives'
import { Button, Dialog, type DialogProps, P, Text } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { RiCheckLine, RiFileCopyLine, RiSpam2Fill } from '@remixicon/react'
// fixme - duplication with the InviteDialog
export const LaterMeetingDialog = ({
@@ -11,19 +12,17 @@ export const LaterMeetingDialog = ({
}: { roomId: string } & Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('home')
const roomUrl = getRouteUrl('room', roomId)
const copyLabel = t('laterMeetingDialog.copy')
const copiedLabel = t('laterMeetingDialog.copied')
const [copyLinkLabel, setCopyLinkLabel] = useState(copyLabel)
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
if (copyLinkLabel == copiedLabel) {
const timeout = setTimeout(() => {
setCopyLinkLabel(copyLabel)
}, 5000)
return () => {
clearTimeout(timeout)
}
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [copyLinkLabel, copyLabel, copiedLabel])
}, [isCopied])
const [isHovered, setIsHovered] = useState(false)
return (
<Dialog
@@ -32,31 +31,63 @@ export const LaterMeetingDialog = ({
title={t('laterMeetingDialog.heading')}
>
<P>{t('laterMeetingDialog.description')}</P>
<HStack alignItems="stretch" gap="gutter">
<Div flex="1">
<Input
type="text"
aria-label={t('laterMeetingDialog.inputLabel')}
value={roomUrl}
readOnly
onClick={(e) => {
e.currentTarget.select()
}}
/>
</Div>
<Div minWidth="8rem">
<Button
variant="primary"
size="sm"
fullWidth
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setCopyLinkLabel(copiedLabel)
}}
>
{copyLinkLabel}
</Button>
</Div>
<Button
variant={isCopied ? 'success' : 'primary'}
size="sm"
fullWidth
aria-label={t('laterMeetingDialog.copy')}
style={{
justifyContent: 'start',
}}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
onHoverChange={setIsHovered}
data-attr="later-dialog-copy"
>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('laterMeetingDialog.copied')}
</>
) : (
<>
<RiFileCopyLine
size={18}
style={{ marginRight: '8px', minWidth: '18px' }}
/>
{isHovered ? (
t('laterMeetingDialog.copy')
) : (
<div
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'none',
textWrap: 'nowrap',
}}
>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
)}
</>
)}
</Button>
<HStack>
<div
style={{
backgroundColor: '#d9e5ff',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
}}
>
<RiSpam2Fill size={22} style={{ fill: '#4c84fc' }} />
</div>
<Text variant="sm" style={{ marginTop: '1rem' }}>
{t('laterMeetingDialog.permissions')}
</Text>
</HStack>
</Dialog>
)
+17 -11
View File
@@ -1,18 +1,18 @@
import { useTranslation } from 'react-i18next'
import { DialogTrigger } from 'react-aria-components'
import { DialogTrigger, MenuItem, Menu as RACMenu } from 'react-aria-components'
import { Button, Menu, Text } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { navigateTo } from '@/navigation/navigateTo'
import { Screen } from '@/layout/Screen'
import { Centered } from '@/layout/Centered'
import { generateRoomId } from '@/features/rooms'
import { authUrl, useUser, UserAware } from '@/features/auth'
import { useUser, UserAware } from '@/features/auth'
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
import { ProConnectButton } from '@/components/ProConnectButton'
import { useCreateRoom } from '@/features/rooms'
import { usePersistentUserChoices } from '@livekit/components-react'
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
import { RiAddLine, RiLink } from '@remixicon/react'
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
import { useState } from 'react'
@@ -27,8 +27,6 @@ export const Home = () => {
const { mutateAsync: createRoom } = useCreateRoom()
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
console.log(laterRoomId)
return (
<UserAware>
<Screen>
@@ -44,10 +42,12 @@ export const Home = () => {
{t('loginToCreateMeeting')}
</Text>
)}
<HStack gap="gutter">
<HStack gap="gutter" alignItems="start">
{isLoggedIn ? (
<Menu>
<Button variant="primary">{t('createMeeting')}</Button>
<Button variant="primary" data-attr="create-meeting">
{t('createMeeting')}
</Button>
<RACMenu>
<MenuItem
className={menuItemRecipe({ icon: true })}
@@ -59,6 +59,7 @@ export const Home = () => {
})
)
}}
data-attr="create-option-instant"
>
<RiAddLine size={18} />
{t('createMenu.instantOption')}
@@ -71,6 +72,7 @@ export const Home = () => {
setLaterRoomId(data.slug)
)
}}
data-attr="create-option-later"
>
<RiLink size={18} />
{t('createMenu.laterOption')}
@@ -78,12 +80,16 @@ export const Home = () => {
</RACMenu>
</Menu>
) : (
<Button variant="primary" href={authUrl()}>
{t('login', { ns: 'global' })}
</Button>
<ProConnectButton />
)}
<DialogTrigger>
<Button variant="primary" outline>
<Button
variant="primary"
outline
style={{
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
}}
>
{t('joinMeeting')}
</Button>
<JoinMeetingDialog />
@@ -0,0 +1,112 @@
import { useEffect } from 'react'
import { useRoomContext } from '@livekit/components-react'
import { Participant, RoomEvent } from 'livekit-client'
import { ToastProvider, toastQueue } from './components/ToastProvider'
import { NotificationType } from './NotificationType'
import { Div } from '@/primitives'
import { isMobileBrowser } from '@livekit/components-core'
import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification'
export const MainNotificationToast = () => {
const room = useRoomContext()
const { triggerNotificationSound } = useNotificationSound()
useEffect(() => {
const showJoinNotification = (participant: Participant) => {
if (isMobileBrowser()) {
return
}
triggerNotificationSound(NotificationType.Joined)
toastQueue.add(
{
participant,
type: NotificationType.Joined,
},
{
timeout: 5000,
}
)
}
room.on(RoomEvent.ParticipantConnected, showJoinNotification)
return () => {
room.off(RoomEvent.ParticipantConnected, showJoinNotification)
}
}, [room, triggerNotificationSound])
useEffect(() => {
const removeJoinNotification = (participant: Participant) => {
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
toast.content.participant === participant &&
toast.content.type === NotificationType.Joined
)
if (existingToast) {
toastQueue.close(existingToast.key)
}
}
room.on(RoomEvent.ParticipantDisconnected, removeJoinNotification)
return () => {
room.off(RoomEvent.ParticipantConnected, removeJoinNotification)
}
}, [room])
useEffect(() => {
const handleNotificationReceived = (
prevMetadataStr: string | undefined,
participant: Participant
) => {
if (!participant) return
if (isMobileBrowser()) return
if (participant.isLocal) return
const prevMetadata = JSON.parse(prevMetadataStr || '{}')
const metadata = JSON.parse(participant.metadata || '{}')
if (prevMetadata.raised == metadata.raised) return
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
toast.content.participant === participant &&
toast.content.type === NotificationType.Raised
)
if (existingToast && prevMetadata.raised && !metadata.raised) {
toastQueue.close(existingToast.key)
return
}
if (!existingToast && !prevMetadata.raised && metadata.raised) {
triggerNotificationSound(NotificationType.Raised)
toastQueue.add(
{
participant,
type: NotificationType.Raised,
},
{ timeout: 5000 }
)
}
}
room.on(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
return () => {
room.off(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived)
}
}, [room, triggerNotificationSound])
useEffect(() => {
const closeAllToasts = () => {
toastQueue.visibleToasts.forEach(({ key }) => toastQueue.close(key))
}
room.on(RoomEvent.Disconnected, closeAllToasts)
return () => {
room.off(RoomEvent.Disconnected, closeAllToasts)
}
}, [room])
return (
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
<ToastProvider />
</Div>
)
}
@@ -0,0 +1,6 @@
export enum NotificationType {
Joined = 'joined',
Default = 'default',
Raised = 'raised',
Lowered = 'lowered',
}
@@ -0,0 +1,58 @@
import { useToast } from '@react-aria/toast'
import { Button } from '@/primitives'
import { RiCloseLine } from '@remixicon/react'
import { ToastState } from '@react-stately/toast'
import { styled } from '@/styled-system/jsx'
import { useRef } from 'react'
import { ToastData } from './ToastProvider'
import type { QueuedToast } from '@react-stately/toast'
export const StyledToastContainer = styled('div', {
base: {
margin: 0.5,
boxShadow:
'rgba(0, 0, 0, 0.5) 0px 4px 8px 0px, rgba(0, 0, 0, 0.3) 0px 6px 20px 4px',
backgroundColor: '#494c4f',
color: 'white',
borderRadius: '8px',
'&[data-entering]': { animation: 'fade 200ms' },
'&[data-exiting]': { animation: 'fade 150ms reverse ease-in' },
width: 'fit-content',
marginLeft: 'auto',
},
})
const StyledToast = styled('div', {
base: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '1rem',
padding: '10px',
},
})
export interface ToastProps {
key: string
toast: QueuedToast<ToastData>
state: ToastState<ToastData>
}
export function Toast({ state, ...props }: ToastProps) {
const ref = useRef(null)
const { toastProps, contentProps, closeButtonProps } = useToast(
props,
state,
ref
)
return (
<StyledToastContainer {...toastProps} ref={ref}>
<StyledToast>
<div {...contentProps}>{props.toast.content?.message} machine a</div>
<Button square size="sm" invisible {...closeButtonProps}>
<RiCloseLine color="white" />
</Button>
</StyledToast>
</StyledToastContainer>
)
}
@@ -0,0 +1,73 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { Button as RACButton } from 'react-aria-components'
import { Track } from 'livekit-client'
import Source = Track.Source
import { useMaybeLayoutContext } from '@livekit/components-react'
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack, styled } from '@/styled-system/jsx'
import { Div } from '@/primitives'
import { useTranslation } from 'react-i18next'
const ClickableToast = styled(RACButton, {
base: {
cursor: 'pointer',
display: 'flex',
borderRadius: 'inherit',
},
})
export function ToastJoined({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
props,
state,
ref
)
const layoutContext = useMaybeLayoutContext()
const participant = props.toast.content.participant
const trackReference = {
participant,
publication: participant.getTrackPublication(Source.Camera),
source: Source.Camera,
}
const pinParticipant = () => {
layoutContext?.pin.dispatch?.({
msg: 'set_pin',
trackReference,
})
}
return (
<StyledToastContainer {...toastProps} ref={ref}>
<ClickableToast
ref={ref}
onPress={(e) => {
pinParticipant()
closeButtonProps.onPress?.(e)
}}
>
<HStack justify="center" alignItems="center" {...contentProps}>
<Div display="flex" overflow="hidden" width="128" height="72">
<ParticipantTile
trackRef={trackReference}
disableSpeakingIndicator={true}
disableMetadata={true}
style={{
borderRadius: '7px 0 0 7px',
width: '100%',
}}
/>
</Div>
<Div padding={20} {...titleProps}>
{t('joined.description', {
name: participant.name || t('defaultName'),
})}
</Div>
</HStack>
</ClickableToast>
</StyledToastContainer>
)
}
@@ -0,0 +1,28 @@
/* eslint-disable react-refresh/only-export-components */
import { ToastQueue, useToastQueue } from '@react-stately/toast'
import { ToastRegion } from './ToastRegion'
import { Participant } from 'livekit-client'
import { NotificationType } from '../NotificationType'
export interface ToastData {
participant: Participant
type: NotificationType
message?: string
}
// Using a global queue for toasts allows for centralized management and queuing of notifications
// from anywhere in the app, providing greater flexibility in complex scenarios.
export const toastQueue = new ToastQueue<ToastData>({
maxVisibleToasts: 5,
})
export const ToastProvider = ({ ...props }) => {
const state = useToastQueue<ToastData>(toastQueue)
return (
<>
{state.visibleToasts.length > 0 && (
<ToastRegion {...props} state={state} />
)}
</>
)
}
@@ -0,0 +1,66 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { Button, Div } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { RiCloseLine, RiHand } from '@remixicon/react'
import { useWidgetInteraction } from '@/features/rooms/livekit/hooks/useWidgetInteraction'
export function ToastRaised({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
props,
state,
ref
)
const participant = props.toast.content.participant
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={0}
>
<RiHand
color="white"
style={{
marginRight: '1rem',
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
<Div {...titleProps} marginRight={0.5}>
{t('raised.description', {
name: participant.name || t('defaultName'),
})}
</Div>
{!isParticipantsOpen && (
<Button
size="sm"
variant="text"
style={{
color: '#60a5fa',
}}
onPress={(e) => {
toggleParticipants()
closeButtonProps.onPress?.(e)
}}
>
{t('raised.cta')}
</Button>
)}
<Button square size="sm" invisible {...closeButtonProps}>
<RiCloseLine size={18} color="white" />
</Button>
</HStack>
</StyledToastContainer>
)
}
@@ -0,0 +1,30 @@
import { AriaToastRegionProps, useToastRegion } from '@react-aria/toast'
import type { ToastState } from '@react-stately/toast'
import { Toast } from './Toast'
import { useRef } from 'react'
import { NotificationType } from '../NotificationType'
import { ToastJoined } from './ToastJoined'
import { ToastData } from './ToastProvider'
import { ToastRaised } from '@/features/notifications/components/ToastRaised.tsx'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
}
export function ToastRegion({ state, ...props }: ToastRegionProps) {
const ref = useRef(null)
const { regionProps } = useToastRegion(props, state, ref)
return (
<div {...regionProps} ref={ref} className="toast-region">
{state.visibleToasts.map((toast) => {
if (toast.content?.type === NotificationType.Joined) {
return <ToastJoined key={toast.key} toast={toast} state={state} />
}
if (toast.content?.type === NotificationType.Raised) {
return <ToastRaised key={toast.key} toast={toast} state={state} />
}
return <Toast key={toast.key} toast={toast} state={state} />
})}
</div>
)
}
@@ -0,0 +1,18 @@
import useSound from 'use-sound'
// fixme - handle dynamic audio output changes
export const useNotificationSound = () => {
const [play] = useSound('./sounds/notifications.mp3', {
sprite: {
joined: [0, 1150],
raised: [1400, 180],
message: [1580, 300],
waiting: [2039, 710],
success: [2740, 1304],
},
})
const triggerNotificationSound = (type: string) => {
play({ id: type })
}
return { triggerNotificationSound }
}
@@ -9,7 +9,6 @@ import {
import { Room, RoomOptions } from 'livekit-client'
import { keys } from '@/api/queryKeys'
import { queryClient } from '@/api/queryClient'
import { navigateTo } from '@/navigation/navigateTo'
import { Screen } from '@/layout/Screen'
import { QueryAware } from '@/components/QueryAware'
import { ErrorScreen } from '@/components/ErrorScreen'
@@ -19,6 +18,7 @@ import { useCreateRoom } from '../api/createRoom'
import { InviteDialog } from './InviteDialog'
import { VideoConference } from '../livekit/prefabs/VideoConference'
import posthog from 'posthog-js'
export const Conference = ({
roomId,
@@ -31,6 +31,9 @@ export const Conference = ({
mode?: 'join' | 'create'
initialRoomData?: ApiRoom
}) => {
useEffect(() => {
posthog.capture('visit-room', { slug: roomId })
}, [roomId])
const fetchKey = [keys.room, roomId, userConfig.username]
const {
@@ -50,7 +53,6 @@ export const Conference = ({
} = useQuery({
queryKey: fetchKey,
staleTime: 6 * 60 * 60 * 1000, // By default, LiveKit access tokens expire 6 hours after generation
enabled: !initialRoomData,
initialData: initialRoomData,
queryFn: () =>
fetchRoom({
@@ -80,25 +82,6 @@ export const Conference = ({
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
/**
* checks for actual click on the leave button instead of
* relying on LiveKitRoom onDisconnected because onDisconnected
* triggers even on page reload, it's not a user "onLeave" event really.
* Here we want to react to the user actually deciding to leave.
*/
useEffect(() => {
const checkOnLeaveClick = (event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.classList.contains('lk-disconnect-button')) {
navigateTo('feedback')
}
}
document.body.addEventListener('click', checkOnLeaveClick)
return () => {
document.body.removeEventListener('click', checkOnLeaveClick)
}
}, [])
const { t } = useTranslation('rooms')
if (isCreateError) {
// this error screen should be replaced by a proper waiting room for anonymous user.
@@ -4,14 +4,20 @@ import { Div, Button, type DialogProps, P } from '@/primitives'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import { Heading, Dialog } from 'react-aria-components'
import { Text, text } from '@/primitives/Text'
import { RiCloseLine, RiFileCopyLine, RiSpam2Fill } from '@remixicon/react'
import {
RiCheckLine,
RiCloseLine,
RiFileCopyLine,
RiSpam2Fill,
} from '@remixicon/react'
import { useEffect, useState } from 'react'
// fixme - extract in a proper primitive this dialog without overlay
const StyledRACDialog = styled(Dialog, {
base: {
position: 'fixed',
left: 30,
bottom: 90,
left: '0.75rem',
bottom: 80,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
@@ -34,17 +40,25 @@ export const InviteDialog = ({
const { t } = useTranslation('rooms')
const roomUrl = getRouteUrl('room', roomId)
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isCopied])
const [isHovered, setIsHovered] = useState(false)
return (
<StyledRACDialog {...dialogProps}>
{({ close }) => (
<VStack
alignItems={'left'}
alignItems="left"
justify="start"
gap={0}
style={{
maxWidth: '100%',
overflow: 'hidden',
}}
style={{ maxWidth: '100%', overflow: 'hidden' }}
>
<Heading slot="title" level={3} className={text({ variant: 'h2' })}>
{t('shareDialog.heading')}
@@ -63,36 +77,49 @@ export const InviteDialog = ({
</Button>
</Div>
<P>{t('shareDialog.description')}</P>
<HStack
justify={'space-between'}
alignItems="center"
<Button
variant={isCopied ? 'success' : 'primary'}
size="sm"
fullWidth
aria-label={t('shareDialog.copy')}
style={{
backgroundColor: '#f1f3f4',
borderRadius: '4px',
maxWidth: '100%',
justifyContent: 'start',
}}
gap={0}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
onHoverChange={setIsHovered}
data-attr="share-dialog-copy"
>
<div
style={{
paddingLeft: '0.75rem',
textOverflow: 'ellipsis',
overflow: 'hidden',
textWrap: 'nowrap',
userSelect: 'none',
}}
>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
<Button
square
invisible
tooltip={t('shareDialog.copy')}
onPress={() => navigator.clipboard.writeText(roomUrl)}
>
<RiFileCopyLine size={24} />
</Button>
</HStack>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('shareDialog.copied')}
</>
) : (
<>
<RiFileCopyLine
size={18}
style={{ marginRight: '8px', minWidth: '18px' }}
/>
{isHovered ? (
t('shareDialog.copy')
) : (
<div
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'none',
textWrap: 'nowrap',
}}
>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
)}
</>
)}
</Button>
<HStack>
<div
style={{
@@ -16,8 +16,8 @@ export const Join = ({
<PreJoin
persistUserChoices
onSubmit={onSubmit}
micLabel={t('join.micLabel')}
camLabel={t('join.camlabel')}
micLabel={t('join.audioinput.label')}
camLabel={t('join.videoinput.label')}
joinLabel={t('join.joinLabel')}
userLabel={t('join.userLabel')}
/>
@@ -0,0 +1,33 @@
import { Participant } from 'livekit-client'
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
export const useLowerHandParticipant = () => {
const data = useRoomData()
const lowerHandParticipant = (participant: Participant) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
const newMetadata = JSON.parse(participant.metadata || '{}')
newMetadata.raised = !newMetadata.raised
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/UpdateParticipant'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
metadata: JSON.stringify(newMetadata),
permission: participant.permissions,
}),
}
)
}
return { lowerHandParticipant }
}
@@ -0,0 +1,19 @@
import { Participant } from 'livekit-client'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant'
export const useLowerHandParticipants = () => {
const { lowerHandParticipant } = useLowerHandParticipant()
const lowerHandParticipants = (participants: Array<Participant>) => {
try {
const promises = participants.map((participant) =>
lowerHandParticipant(participant)
)
return Promise.all(promises)
} catch (error) {
console.error('An error occurred while lowering hands :', error)
throw new Error('An error occurred while lowering hands.')
}
}
return { lowerHandParticipants }
}
@@ -0,0 +1,191 @@
import { useEffect, useRef, useState } from 'react'
import { useLocalParticipant } from '@livekit/components-react'
import { LocalVideoTrack } from 'livekit-client'
import { Text, P, ToggleButton, Div, H } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import {
BackgroundBlur,
BackgroundOptions,
ProcessorWrapper,
BackgroundTransformer,
} from '@livekit/track-processors'
const Information = styled('div', {
base: {
backgroundColor: 'orange.50',
borderRadius: '4px',
padding: '0.75rem 0.75rem',
marginTop: '0.8rem',
alignItems: 'start',
},
})
enum BlurRadius {
NONE = 0,
LIGHT = 5,
NORMAL = 10,
}
export const Effects = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
const { isCameraEnabled, cameraTrack, localParticipant } =
useLocalParticipant()
const videoRef = useRef<HTMLVideoElement>(null)
const [processorPending, setProcessorPending] = useState(false)
const localCameraTrack = cameraTrack?.track as LocalVideoTrack
const getProcessor = () => {
return localCameraTrack?.getProcessor() as ProcessorWrapper<BackgroundOptions>
}
const getBlurRadius = (): BlurRadius => {
const processor = getProcessor()
return (
(processor?.transformer as BackgroundTransformer)?.options?.blurRadius ||
BlurRadius.NONE
)
}
const toggleBlur = async (blurRadius: number) => {
if (!isCameraEnabled) await localParticipant.setCameraEnabled(true)
if (!localCameraTrack) return
setProcessorPending(true)
const processor = getProcessor()
const currentBlurRadius = getBlurRadius()
try {
if (blurRadius == currentBlurRadius && processor) {
await localCameraTrack.stopProcessor()
} else if (!processor) {
await localCameraTrack.setProcessor(BackgroundBlur(blurRadius))
} else {
await processor?.updateTransformerOptions({ blurRadius })
}
} catch (error) {
console.error('Error applying blur:', error)
} finally {
setProcessorPending(false)
}
}
useEffect(() => {
const videoElement = videoRef.current
if (!videoElement) return
const attachVideoTrack = async () => localCameraTrack?.attach(videoElement)
attachVideoTrack()
return () => {
if (!videoElement) return
localCameraTrack.detach(videoElement)
}
}, [localCameraTrack, isCameraEnabled])
const isSelected = (blurRadius: BlurRadius) => {
return isCameraEnabled && getBlurRadius() == blurRadius
}
const tooltipLabel = (blurRadius: BlurRadius) => {
return t(`blur.${isSelected(blurRadius) ? 'clear' : 'apply'}`)
}
return (
<VStack padding="0 1.5rem">
{localCameraTrack && isCameraEnabled ? (
<video
ref={videoRef}
width="100%"
muted
style={{
transform: 'rotateY(180deg)',
minHeight: '175px',
borderRadius: '8px',
}}
/>
) : (
<div
style={{
width: '100%',
height: '174px',
display: 'flex',
backgroundColor: 'black',
justifyContent: 'center',
flexDirection: 'column',
}}
>
<P
style={{
color: 'white',
textAlign: 'center',
textWrap: 'balance',
marginBottom: 0,
}}
>
{t('activateCamera')}
</P>
</div>
)}
<Div
alignItems={'left'}
width={'100%'}
style={{
border: '1px solid #dadce0',
borderRadius: '8px',
margin: '0 .625rem',
padding: '0.5rem 1rem',
}}
>
<H
lvl={3}
style={{
marginBottom: '0.4rem',
}}
>
{t('heading')}
</H>
{ProcessorWrapper.isSupported ? (
<HStack>
<ToggleButton
size={'sm'}
legacyStyle
aria-label={tooltipLabel(BlurRadius.LIGHT)}
tooltip={tooltipLabel(BlurRadius.LIGHT)}
isDisabled={processorPending}
onPress={async () => await toggleBlur(BlurRadius.LIGHT)}
isSelected={isSelected(BlurRadius.LIGHT)}
>
{t('blur.light')}
</ToggleButton>
<ToggleButton
size={'sm'}
legacyStyle
aria-label={tooltipLabel(BlurRadius.NORMAL)}
tooltip={tooltipLabel(BlurRadius.NORMAL)}
isDisabled={processorPending}
onPress={async () => await toggleBlur(BlurRadius.NORMAL)}
isSelected={isSelected(BlurRadius.NORMAL)}
>
{t('blur.normal')}
</ToggleButton>
</HStack>
) : (
<Text variant="sm">{t('notAvailable')}</Text>
)}
<Information>
<Text
variant="sm"
style={{
textWrap: 'balance',
}}
>
{t('experimental')}
</Text>
</Information>
</Div>
</VStack>
)
}
@@ -0,0 +1,26 @@
import { useTrackMutedIndicator } from '@livekit/components-react'
import { Participant, Track } from 'livekit-client'
import Source = Track.Source
import { Div } from '@/primitives'
import { RiMicOffFill } from '@remixicon/react'
export const MutedMicIndicator = ({
participant,
}: {
participant: Participant
}) => {
const { isMuted } = useTrackMutedIndicator({
participant: participant,
source: Source.Microphone,
})
if (!isMuted) {
return null
}
return (
<Div padding={0.25} backgroundColor="red" borderRadius="4px">
<RiMicOffFill size={16} color="white" />
</Div>
)
}
@@ -6,7 +6,6 @@ import {
ParticipantName,
ParticipantTileProps,
ScreenShareIcon,
TrackMutedIndicator,
useEnsureTrackRef,
useFeatureContext,
useIsEncrypted,
@@ -25,6 +24,10 @@ import {
} from '@livekit/components-core'
import { Track } from 'livekit-client'
import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder'
import { RiHand } from '@remixicon/react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { HStack } from '@/styled-system/jsx'
import { MutedMicIndicator } from '@/features/rooms/livekit/components/MutedMicIndicator'
export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{
@@ -41,19 +44,24 @@ export function TrackRefContextIfNeeded(
)
}
interface ParticipantTileExtendedProps extends ParticipantTileProps {
disableMetadata?: boolean
}
export const ParticipantTile: (
props: ParticipantTileProps & React.RefAttributes<HTMLDivElement>
props: ParticipantTileExtendedProps & React.RefAttributes<HTMLDivElement>
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
HTMLDivElement,
ParticipantTileProps
ParticipantTileExtendedProps
>(function ParticipantTile(
{
trackRef,
children,
onParticipantClick,
disableSpeakingIndicator,
disableMetadata,
...htmlProps
}: ParticipantTileProps,
}: ParticipantTileExtendedProps,
ref
) {
const trackReference = useEnsureTrackRef(trackRef)
@@ -84,6 +92,10 @@ export const ParticipantTile: (
[trackReference, layoutContext]
)
const { isHandRaised } = useRaisedHand({
participant: trackReference.participant,
})
return (
<div ref={ref} style={{ position: 'relative' }} {...elementProps}>
<TrackRefContextIfNeeded trackRef={trackReference}>
@@ -112,34 +124,56 @@ export const ParticipantTile: (
participant={trackReference.participant}
/>
</div>
<div className="lk-participant-metadata">
<div className="lk-participant-metadata-item">
{trackReference.source === Track.Source.Camera ? (
<>
{isEncrypted && (
<LockLockedIcon style={{ marginRight: '0.25rem' }} />
{!disableMetadata && (
<div className="lk-participant-metadata">
<HStack gap={0.25}>
<MutedMicIndicator
participant={trackReference.participant}
/>
<div
className="lk-participant-metadata-item"
style={{
minHeight: '24px',
backgroundColor: isHandRaised ? 'white' : undefined,
color: isHandRaised ? 'black' : undefined,
transition: 'background 200ms ease, color 400ms ease',
}}
>
{trackReference.source === Track.Source.Camera ? (
<>
{isHandRaised && (
<RiHand
color="black"
size={16}
style={{
marginInlineEnd: '.25rem', // fixme - match TrackMutedIndicator styling
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
)}
{isEncrypted && (
<LockLockedIcon
style={{ marginRight: '0.25rem' }}
/>
)}
<ParticipantName />
</>
) : (
<>
<ScreenShareIcon style={{ marginRight: '0.25rem' }} />
<ParticipantName>&apos;s screen</ParticipantName>
</>
)}
<TrackMutedIndicator
trackRef={{
participant: trackReference.participant,
source: Track.Source.Microphone,
}}
show={'muted'}
></TrackMutedIndicator>
<ParticipantName />
</>
) : (
<>
<ScreenShareIcon style={{ marginRight: '0.25rem' }} />
<ParticipantName>&apos;s screen</ParticipantName>
</>
)}
</div>
</HStack>
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
</div>
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
</div>
)}
</>
)}
<FocusToggle trackRef={trackReference} />
{!disableMetadata && <FocusToggle trackRef={trackReference} />}
</ParticipantContextIfNeeded>
</TrackRefContextIfNeeded>
</div>
@@ -0,0 +1,88 @@
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text'
import { Box, Button, Div } from '@/primitives'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useWidgetInteraction } from '../hooks/useWidgetInteraction'
import { ReactNode } from 'react'
import { Effects } from './Effects'
type StyledSidePanelProps = {
title: string
children: ReactNode
onClose: () => void
closeButtonTooltip: string
}
const StyledSidePanel = ({
title,
children,
onClose,
closeButtonTooltip,
}: StyledSidePanelProps) => (
<Box
size="sm"
minWidth="360px"
className={css({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
margin: '1.5rem 1.5rem 1.5rem 0',
padding: 0,
gap: 0,
})}
>
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
}}
>
{title}
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
<Div overflowY="scroll">{children}</Div>
</Box>
)
export const SidePanel = () => {
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const { isParticipantsOpen, isEffectsOpen } = useWidgetInteraction()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
if (!sidePanel) {
return
}
return (
<StyledSidePanel
title={t(`heading.${sidePanel}`)}
onClose={() => (layoutStore.sidePanel = null)}
closeButtonTooltip={t('closeButton', {
content: t(`content.${sidePanel}`),
})}
>
{isParticipantsOpen && <ParticipantsList />}
{isEffectsOpen && <Effects />}
</StyledSidePanel>
)
}
@@ -2,18 +2,13 @@ import { useTranslation } from 'react-i18next'
import { RiChat1Line } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useLayoutContext } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants'
import { useWidgetInteraction } from '../../hooks/useWidgetInteraction'
export const ChatToggle = () => {
const { t } = useTranslation('rooms')
const { t } = useTranslation('rooms', { keyPrefix: 'controls.chat' })
const { dispatch, state } = useLayoutContext().widget
const tooltipLabel = state?.showChat ? 'open' : 'closed'
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
const { isChatOpen, unreadMessages, toggleChat } = useWidgetInteraction()
const tooltipLabel = isChatOpen ? 'open' : 'closed'
return (
<div
@@ -25,17 +20,15 @@ export const ChatToggle = () => {
<ToggleButton
square
legacyStyle
aria-label={t(`controls.chat.${tooltipLabel}`)}
tooltip={t(`controls.chat.${tooltipLabel}`)}
isSelected={state?.showChat}
onPress={() => {
if (showParticipants) participantsStore.showParticipants = false
if (dispatch) dispatch({ msg: 'toggle_chat' })
}}
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isChatOpen}
onPress={() => toggleChat()}
data-attr={`controls-chat-${tooltipLabel}`}
>
<RiChat1Line />
</ToggleButton>
{!!state?.unreadMessages && (
{!!unreadMessages && (
<div
className={css({
position: 'absolute',
@@ -0,0 +1,38 @@
import { useTranslation } from 'react-i18next'
import { RiHand } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useRoomContext } from '@livekit/components-react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
export const HandToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
const room = useRoomContext()
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
participant: room.localParticipant,
})
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
return (
<div
className={css({
position: 'relative',
display: 'inline-block',
})}
>
<ToggleButton
square
legacyStyle
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isHandRaised}
onPress={() => toggleRaisedHand()}
data-attr={`controls-hand-${tooltipLabel}`}
>
<RiHand />
</ToggleButton>
</div>
)
}
@@ -0,0 +1,37 @@
import { useConnectionState, useRoomContext } from '@livekit/components-react'
import { Button } from '@/primitives'
import { navigateTo } from '@/navigation/navigateTo'
import { RiPhoneFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ConnectionState } from 'livekit-client'
export const LeaveButton = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
const room = useRoomContext()
const connectionState = useConnectionState(room)
return (
<Button
isDisabled={connectionState === ConnectionState.Disconnected}
variant={'danger'}
tooltip={t('leave')}
aria-label={t('leave')}
onPress={() => {
room
.disconnect(true)
.catch((e) =>
console.error('An error occurred while disconnecting:', e)
)
.finally(() => {
navigateTo('feedback')
})
}}
data-attr="controls-leave"
>
<RiPhoneFill
style={{
transform: 'rotate(135deg)',
}}
/>
</Button>
)
}
@@ -1,13 +1,15 @@
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
import {
RiAccountBoxLine,
RiFeedbackLine,
RiQuestionLine,
RiSettings3Line,
} from '@remixicon/react'
import { MenuItem, Menu as RACMenu } from 'react-aria-components'
import { MenuItem, Menu as RACMenu, Section } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { Dispatch, SetStateAction } from 'react'
import { DialogState } from '@/features/rooms/livekit/components/controls/Options/OptionsButton'
import { DialogState } from './OptionsButton'
import { Separator } from '@/primitives/Separator'
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = ({
@@ -15,8 +17,8 @@ export const OptionsMenuItems = ({
}: {
onOpenDialog: Dispatch<SetStateAction<DialogState>>
}) => {
const { t } = useTranslation('rooms')
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { toggleEffects } = useWidgetInteraction()
return (
<RACMenu
style={{
@@ -24,29 +26,33 @@ export const OptionsMenuItems = ({
width: '300px',
}}
>
<MenuItem
href="https://tchap.gouv.fr/#/room/!aGImQayAgBLjSBycpm:agent.dinum.tchap.gouv.fr?via=agent.dinum.tchap.gouv.fr"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiQuestionLine size={18} />
{t('options.items.support')}
</MenuItem>
<MenuItem
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiFeedbackLine size={18} />
{t('options.items.feedbacks')}
</MenuItem>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={() => onOpenDialog('settings')}
>
<RiSettings3Line size={18} />
{t('options.items.settings')}
</MenuItem>
<Section>
<MenuItem
onAction={() => toggleEffects()}
className={menuItemRecipe({ icon: true })}
>
<RiAccountBoxLine size={20} />
{t('effects')}
</MenuItem>
</Section>
<Separator />
<Section>
<MenuItem
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
target="_blank"
className={menuItemRecipe({ icon: true })}
>
<RiFeedbackLine size={20} />
{t('feedbacks')}
</MenuItem>
<MenuItem
className={menuItemRecipe({ icon: true })}
onAction={() => onOpenDialog('settings')}
>
<RiSettings3Line size={20} />
{t('settings')}
</MenuItem>
</Section>
</RACMenu>
)
}
@@ -0,0 +1,82 @@
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { Text } from '@/primitives/Text'
import { useTranslation } from 'react-i18next'
import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import { RiHand } from '@remixicon/react'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant.ts'
import { Button } from '@/primitives'
type HandRaisedListItemProps = {
participant: Participant
}
export const HandRaisedListItem = ({
participant,
}: HandRaisedListItemProps) => {
const { t } = useTranslation('rooms')
const name = participant.name || participant.identity
const { lowerHandParticipant } = useLowerHandParticipant()
return (
<HStack
role="listitem"
justify="space-between"
key={participant.identity}
id={participant.identity}
className={css({
padding: '0.25rem 0',
width: 'full',
})}
>
<HStack>
<Avatar name={name} bgColor={getParticipantColor(participant)} />
<Text
variant={'sm'}
className={css({
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
<span
className={css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
})}
>
{name}
</span>
{isLocal(participant) && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
})}
>
({t('participants.you')})
</span>
)}
</Text>
</HStack>
<Button
square
invisible
size="sm"
onPress={() => lowerHandParticipant(participant)}
tooltip={t('participants.lowerParticipantHand', { name })}
data-attr="participants-lower-hand"
>
<RiHand />
</Button>
</HStack>
)
}
@@ -0,0 +1,27 @@
import { Button } from '@/primitives'
import { useLowerHandParticipants } from '@/features/rooms/livekit/api/lowerHandParticipants'
import { useTranslation } from 'react-i18next'
import { Participant } from 'livekit-client'
type LowerAllHandsButtonProps = {
participants: Array<Participant>
}
export const LowerAllHandsButton = ({
participants,
}: LowerAllHandsButtonProps) => {
const { lowerHandParticipants } = useLowerHandParticipants()
const { t } = useTranslation('rooms')
return (
<Button
aria-label={t('participants.lowerParticipantsHand')}
size="sm"
fullWidth
variant="text"
onPress={() => lowerHandParticipants(participants)}
data-attr="participants-lower-hands"
>
{t('participants.lowerParticipantsHand')}
</Button>
)
}
@@ -7,15 +7,12 @@ import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant, Track } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import { Button as RACButton } from 'react-aria-components'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import {
useIsSpeaking,
useTrackMutedIndicator,
} from '@livekit/components-react'
import Source = Track.Source
import { RiMicOffLine } from '@remixicon/react'
import { TooltipWrapper } from '@/primitives/TooltipWrapper.tsx'
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
import { Button, Dialog, P } from '@/primitives'
import { useState } from 'react'
import { useMuteParticipant } from '@/features/rooms/livekit/api/muteParticipant'
@@ -36,10 +33,10 @@ const MuteAlertDialog = ({
<Dialog isOpen={isOpen} role="alertdialog">
<P>{t('participants.muteParticipantAlert.description', { name })}</P>
<HStack gap={1}>
<Button size="sm" invisible onPress={onClose}>
<Button variant="text" size="sm" onPress={onClose}>
{t('participants.muteParticipantAlert.cancel')}
</Button>
<Button size="sm" invisible onPress={onSubmit}>
<Button variant="text" size="sm" onPress={onSubmit}>
{t('participants.muteParticipantAlert.confirm')}
</Button>
</HStack>
@@ -59,45 +56,40 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
source: Source.Microphone,
})
const isSpeaking = useIsSpeaking(participant)
const isDisabled = isLocal(participant) || (!isLocal(participant) && isMuted)
const [isAlertOpen, setIsAlertOpen] = useState(false)
const name = participant.name || participant.identity
return (
<>
<TooltipWrapper
tooltip={t('participants.muteParticipant', {
name,
})}
tooltipType="instant"
<Button
square
invisible
size="sm"
tooltip={
isLocal(participant)
? t('participants.muteYourself')
: t('participants.muteParticipant', {
name,
})
}
isDisabled={isMuted}
onPress={() =>
!isMuted && isLocal(participant)
? muteParticipant(participant)
: setIsAlertOpen(true)
}
data-attr="participants-mute"
>
<RACButton
isDisabled={isDisabled}
className={css({
padding: '10px',
minWidth: '24px',
minHeight: '24px',
borderRadius: '50%',
backgroundColor: 'transparent',
transition: 'background 200ms',
'&[data-hovered]': {
backgroundColor: '#f5f5f5',
},
'&[data-focused]': {
backgroundColor: '#f5f5f5',
},
})}
onPress={() => !isMuted && setIsAlertOpen(true)}
>
{isMuted ? (
<RiMicOffLine color="gray" />
) : (
<ActiveSpeaker isSpeaking={isSpeaking} />
)}
</RACButton>
</TooltipWrapper>
{isMuted ? (
<RiMicOffFill color={'gray'} />
) : (
<RiMicFill
style={{
animation: isSpeaking ? 'pulse_mic 800ms infinite' : undefined,
}}
/>
)}
</Button>
<MuteAlertDialog
isOpen={isAlertOpen}
onSubmit={() =>
@@ -119,7 +111,6 @@ export const ParticipantListItem = ({
}: ParticipantListItemProps) => {
const { t } = useTranslation('rooms')
const name = participant.name || participant.identity
return (
<HStack
role="listitem"
@@ -0,0 +1,109 @@
import { useState } from 'react'
import { css } from '@/styled-system/css'
import { ToggleButton } from 'react-aria-components'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import { RiArrowUpSLine } from '@remixicon/react'
import { Participant } from 'livekit-client'
import { useTranslation } from 'react-i18next'
const ToggleHeader = styled(ToggleButton, {
base: {
minHeight: '40px', //fixme hardcoded value
paddingRight: '.5rem',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
width: '100%',
alignItems: 'center',
transition: 'background 200ms',
borderTopRadius: '7px',
'&[data-hovered]': {
backgroundColor: '#f5f5f5',
},
},
})
const Container = styled('div', {
base: {
border: '1px solid #dadce0',
borderRadius: '8px',
margin: '0 .625rem',
},
})
const ListContainer = styled(VStack, {
base: {
borderTop: '1px solid #dadce0',
alignItems: 'start',
overflowY: 'scroll',
overflowX: 'hidden',
minHeight: 0,
flexGrow: 1,
display: 'flex',
paddingY: '0.5rem',
paddingX: '1rem',
gap: 0,
},
})
type ParticipantsCollapsableListProps = {
heading: string
participants: Array<Participant>
renderParticipant: (participant: Participant) => JSX.Element
action?: () => JSX.Element
}
export const ParticipantsCollapsableList = ({
heading,
participants,
renderParticipant,
action,
}: ParticipantsCollapsableListProps) => {
const { t } = useTranslation('rooms')
const [isOpen, setIsOpen] = useState(true)
const label = t(`participants.collapsable.${isOpen ? 'close' : 'open'}`, {
name: heading,
})
return (
<Container>
<ToggleHeader
isSelected={isOpen}
aria-label={label}
onPress={() => setIsOpen(!isOpen)}
style={{
borderRadius: !isOpen ? '7px' : undefined,
}}
>
<HStack
justify="space-between"
className={css({
margin: '0 1.25rem',
width: '100%',
})}
>
<div
className={css({
fontSize: '1rem',
})}
>
{heading}
</div>
<div>{participants?.length || 0}</div>
</HStack>
<RiArrowUpSLine
size={32}
style={{
transform: isOpen ? 'rotate(-180deg)' : undefined,
transition: 'transform 200ms',
}}
/>
</ToggleHeader>
{isOpen && (
<ListContainer>
{action && action()}
{participants.map((participant) => renderParticipant(participant))}
</ListContainer>
)}
</Container>
)
}
@@ -1,25 +1,27 @@
import { css } from '@/styled-system/css'
import { allParticipantRoomEvents } from '@livekit/components-core'
import { useParticipants } from '@livekit/components-react'
import { Heading } from 'react-aria-components'
import { Box, Button, Div } from '@/primitives'
import { text } from '@/primitives/Text'
import { RiCloseLine } from '@remixicon/react'
import { participantsStore } from '@/stores/participants'
import { Div, H } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
import { VStack } from '@/styled-system/jsx'
import { ParticipantListItem } from '../../controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '../../controls/Participants/ParticipantsCollapsableList'
import { HandRaisedListItem } from '../../controls/Participants/HandRaisedListItem'
import { LowerAllHandsButton } from '../../controls/Participants/LowerAllHandsButton'
import { RoomEvent } from 'livekit-client'
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
export const ParticipantsList = () => {
const { t } = useTranslation('rooms')
const { t } = useTranslation('rooms', { keyPrefix: 'participants' })
// Preferred using the 'useParticipants' hook rather than the separate remote and local hooks,
// because the 'useLocalParticipant' hook does not update the participant's information when their
// metadata/name changes. The LiveKit team has marked this as a TODO item in the code.
const participants = useParticipants({
updateOnlyOn: allParticipantRoomEvents,
updateOnlyOn: [
RoomEvent.ParticipantNameChanged,
...allParticipantRoomEvents,
],
})
const sortedRemoteParticipants = participants
@@ -35,59 +37,47 @@ export const ParticipantsList = () => {
...sortedRemoteParticipants,
]
const raisedHandParticipants = participants.filter((participant) => {
const data = JSON.parse(participant.metadata || '{}')
return data.raised
})
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
return (
<Box
size="sm"
minWidth="300px"
className={css({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
margin: '1.5rem 1.5rem 1.5rem 0',
})}
>
<Heading slot="title" level={3} className={text({ variant: 'h2' })}>
<span>{t('participants.heading')}</span>{' '}
<span
className={css({
marginLeft: '0.75rem',
fontWeight: 'normal',
fontSize: '1rem',
})}
>
{participants?.length}
</span>
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={() => (participantsStore.showParticipants = false)}
aria-label={t('participants.closeButton')}
tooltip={t('participants.closeButton')}
>
<RiCloseLine />
</Button>
</Div>
{sortedParticipants?.length > 0 && (
<VStack
role="list"
className={css({
alignItems: 'start',
gap: 'none',
overflowY: 'scroll',
overflowX: 'hidden',
minHeight: 0,
flexGrow: 1,
display: 'flex',
})}
>
{sortedParticipants.map((participant) => (
<ParticipantListItem participant={participant} />
))}
</VStack>
<>
<H
lvl={2}
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: '#5f6368',
padding: '0 1.5rem',
marginBottom: '0.83em',
})}
>
{t('subheading').toUpperCase()}
</H>
{raisedHandParticipants.length > 0 && (
<Div marginBottom=".9375rem">
<ParticipantsCollapsableList
heading={t('raisedHands')}
participants={raisedHandParticipants}
renderParticipant={(participant) => (
<HandRaisedListItem participant={participant} />
)}
action={() => (
<LowerAllHandsButton participants={raisedHandParticipants} />
)}
/>
</Div>
)}
</Box>
<ParticipantsCollapsableList
heading={t('contributors')}
participants={sortedParticipants}
renderParticipant={(participant) => (
<ParticipantListItem participant={participant} />
)}
/>
</>
)
}
@@ -2,14 +2,11 @@ import { useTranslation } from 'react-i18next'
import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useLayoutContext, useParticipants } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants'
import { useParticipants } from '@livekit/components-react'
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
export const ParticipantsToggle = () => {
const { t } = useTranslation('rooms')
const { dispatch, state } = useLayoutContext().widget
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
/**
* Context could not be used due to inconsistent refresh behavior.
@@ -19,10 +16,9 @@ export const ParticipantsToggle = () => {
const participants = useParticipants()
const numParticipants = participants?.length
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
const tooltipLabel = showParticipants ? 'open' : 'closed'
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
return (
<div
@@ -34,13 +30,11 @@ export const ParticipantsToggle = () => {
<ToggleButton
square
legacyStyle
aria-label={t(`controls.participants.${tooltipLabel}`)}
tooltip={t(`controls.participants.${tooltipLabel}`)}
isSelected={showParticipants}
onPress={() => {
if (dispatch && state?.showChat) dispatch({ msg: 'toggle_chat' })
participantsStore.showParticipants = !showParticipants
}}
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isParticipantsOpen}
onPress={() => toggleParticipants()}
data-attr={`controls-participants-${tooltipLabel}`}
>
<RiGroupLine />
</ToggleButton>
@@ -0,0 +1,56 @@
import { Div, ToggleButton } from '@/primitives'
import { RiArrowUpLine, RiCloseFill, RiRectangleLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
import { Track } from 'livekit-client'
import React from 'react'
export const ScreenShareToggle = (
props: Omit<
UseTrackToggleProps<Track.Source.ScreenShare>,
'source' | 'captureOptions'
>
) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.screenShare' })
const { buttonProps, enabled } = useTrackToggle({
...props,
source: Track.Source.ScreenShare,
captureOptions: { audio: true, selfBrowserSurface: 'include' },
})
const tooltipLabel = enabled ? 'stop' : 'start'
const Icon = enabled ? RiCloseFill : RiArrowUpLine
// fixme - remove ToggleButton custom styles when we design a proper icon
return (
<ToggleButton
isSelected={enabled}
square
legacyStyle
tooltip={t(tooltipLabel)}
onPress={(e) =>
buttonProps.onClick?.(
e as unknown as React.MouseEvent<HTMLButtonElement, MouseEvent>
)
}
style={{
maxWidth: '46px',
maxHeight: '46px',
}}
data-attr={`controls-screenshare-${tooltipLabel}`}
>
<Div position="relative">
<RiRectangleLine size={28} />
<Icon
size={16}
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
/>
</Div>
</ToggleButton>
)
}
@@ -0,0 +1,120 @@
import { useTranslation } from 'react-i18next'
import {
useMediaDeviceSelect,
useTrackToggle,
UseTrackToggleProps,
} from '@livekit/components-react'
import { HStack } from '@/styled-system/jsx'
import { Button, Menu, MenuList, ToggleButton } from '@/primitives'
import {
RemixiconComponentType,
RiArrowDownSLine,
RiMicLine,
RiMicOffLine,
RiVideoOffLine,
RiVideoOnLine,
} from '@remixicon/react'
import { Track } from 'livekit-client'
import React from 'react'
export type ToggleSource = Exclude<
Track.Source,
Track.Source.ScreenShareAudio | Track.Source.Unknown
>
type SelectToggleSource = Exclude<ToggleSource, Track.Source.ScreenShare>
type SelectToggleDeviceConfig = {
kind: MediaDeviceKind
iconOn: RemixiconComponentType
iconOff: RemixiconComponentType
}
type SelectToggleDeviceConfigMap = {
[key in SelectToggleSource]: SelectToggleDeviceConfig
}
const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
[Track.Source.Microphone]: {
kind: 'audioinput',
iconOn: RiMicLine,
iconOff: RiMicOffLine,
},
[Track.Source.Camera]: {
kind: 'videoinput',
iconOn: RiVideoOnLine,
iconOff: RiVideoOffLine,
},
}
type SelectToggleDeviceProps<T extends ToggleSource> =
UseTrackToggleProps<T> & {
onActiveDeviceChange: (deviceId: string) => void
source: SelectToggleSource
}
export const SelectToggleDevice = <T extends ToggleSource>({
onActiveDeviceChange,
...props
}: SelectToggleDeviceProps<T>) => {
const config = selectToggleDeviceConfig[props.source]
if (!config) {
throw new Error('Invalid source')
}
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { buttonProps, enabled } = useTrackToggle(props)
const { kind, iconOn, iconOff } = config
const { devices, activeDeviceId, setActiveMediaDevice } =
useMediaDeviceSelect({ kind })
const toggleLabel = t(enabled ? 'disable' : 'enable', {
keyPrefix: `join.${kind}`,
})
const selectLabel = t('choose', { keyPrefix: `join.${kind}` })
const Icon = enabled ? iconOn : iconOff
return (
<HStack gap={0}>
<ToggleButton
isSelected={enabled}
variant={enabled ? undefined : 'danger'}
toggledStyles={false}
onPress={(e) =>
buttonProps.onClick?.(
e as unknown as React.MouseEvent<HTMLButtonElement>
)
}
aria-label={toggleLabel}
tooltip={toggleLabel}
groupPosition="left"
>
<Icon />
</ToggleButton>
<Menu>
<Button
tooltip={selectLabel}
aria-label={selectLabel}
groupPosition="right"
square
>
<RiArrowDownSLine />
</Button>
<MenuList
items={devices.map((d) => ({
value: d.deviceId,
label: d.label,
}))}
selectedItem={activeDeviceId}
onAction={(value) => {
setActiveMediaDevice(value as string)
onActiveDeviceChange(value as string)
}}
/>
</Menu>
</HStack>
)
}
@@ -1,33 +0,0 @@
import { RoomEvent } from 'livekit-client'
// Issue: 'allRemoteParticipantRoomEvents' is not exposed or importable. One event is missing
// to trigger the real-time update of participants when they change their name.
// This code is duplicated from LiveKit.
export const allRemoteParticipantRoomEvents = [
RoomEvent.ConnectionStateChanged,
RoomEvent.RoomMetadataChanged,
RoomEvent.ActiveSpeakersChanged,
RoomEvent.ConnectionQualityChanged,
RoomEvent.ParticipantConnected,
RoomEvent.ParticipantDisconnected,
RoomEvent.ParticipantPermissionsChanged,
RoomEvent.ParticipantMetadataChanged,
RoomEvent.ParticipantNameChanged, // This element is missing in LiveKit and causes problems
RoomEvent.TrackMuted,
RoomEvent.TrackUnmuted,
RoomEvent.TrackPublished,
RoomEvent.TrackUnpublished,
RoomEvent.TrackStreamStateChanged,
RoomEvent.TrackSubscriptionFailed,
RoomEvent.TrackSubscriptionPermissionChanged,
RoomEvent.TrackSubscriptionStatusChanged,
]
export const allParticipantRoomEvents = [
...allRemoteParticipantRoomEvents,
RoomEvent.LocalTrackPublished,
RoomEvent.LocalTrackUnpublished,
]
@@ -0,0 +1,23 @@
import { LocalParticipant, Participant } from 'livekit-client'
import { useParticipantInfo } from '@livekit/components-react'
import { isLocal } from '@/utils/livekit'
type useRaisedHandProps = {
participant: Participant
}
export function useRaisedHand({ participant }: useRaisedHandProps) {
// fixme - refactor this part to rely on attributes
const { metadata } = useParticipantInfo({ participant })
const parsedMetadata = JSON.parse(metadata || '{}')
const toggleRaisedHand = () => {
if (isLocal(participant)) {
parsedMetadata.raised = !parsedMetadata.raised
const localParticipant = participant as LocalParticipant
localParticipant.setMetadata(JSON.stringify(parsedMetadata))
}
}
return { isHandRaised: parsedMetadata.raised, toggleRaisedHand }
}
@@ -0,0 +1,46 @@
import { useLayoutContext } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
export const useWidgetInteraction = () => {
const { dispatch, state } = useLayoutContext().widget
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const isParticipantsOpen = sidePanel == 'participants'
const isEffectsOpen = sidePanel == 'effects'
const toggleParticipants = () => {
if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' })
}
layoutStore.sidePanel = isParticipantsOpen ? null : 'participants'
}
const toggleChat = () => {
if (isParticipantsOpen) {
layoutStore.sidePanel = null
}
if (dispatch) {
dispatch({ msg: 'toggle_chat' })
}
}
const toggleEffects = () => {
if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' })
}
layoutStore.sidePanel = isEffectsOpen ? null : 'effects'
}
return {
toggleParticipants,
toggleChat,
toggleEffects,
isChatOpen: state?.showChat,
unreadMessages: state?.unreadMessages,
isParticipantsOpen,
isEffectsOpen,
}
}
@@ -4,10 +4,6 @@ import * as React from 'react'
import { supportsScreenSharing } from '@livekit/components-core'
import {
DisconnectButton,
LeaveIcon,
MediaDeviceMenu,
TrackToggle,
useMaybeLayoutContext,
usePersistentUserChoices,
} from '@livekit/components-react'
@@ -15,10 +11,13 @@ import {
import { mergeProps } from '@/utils/mergeProps.ts'
import { StartMediaButton } from '../components/controls/StartMediaButton'
import { useMediaQuery } from '../hooks/useMediaQuery'
import { useTranslation } from 'react-i18next'
import { OptionsButton } from '../components/controls/Options/OptionsButton'
import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
import { ParticipantsToggle } from '../components/controls/Participants/ParticipantsToggle'
import { ChatToggle } from '../components/controls/ChatToggle'
import { HandToggle } from '../components/controls/HandToggle'
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
import { LeaveButton } from '../components/controls/LeaveButton'
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
/** @public */
export type ControlBarControls = {
@@ -37,7 +36,7 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
controls?: ControlBarControls
/**
* If `true`, the user's device choices will be persisted.
* This will enables the user to have the same device choices when they rejoin the room.
* This will enable the user to have the same device choices when they rejoin the room.
* @defaultValue true
* @alpha
*/
@@ -66,7 +65,6 @@ export function ControlBar({
onDeviceError,
...props
}: ControlBarProps) {
const { t } = useTranslation('rooms')
const [isChatOpen, setIsChatOpen] = React.useState(false)
const layoutContext = useMaybeLayoutContext()
React.useEffect(() => {
@@ -82,26 +80,8 @@ export function ControlBar({
const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose'
variation ??= defaultVariation
const showIcon = React.useMemo(
() => variation === 'minimal' || variation === 'verbose',
[variation]
)
const showText = React.useMemo(
() => variation === 'textOnly' || variation === 'verbose',
[variation]
)
const browserSupportsScreenSharing = supportsScreenSharing()
const [isScreenShareEnabled, setIsScreenShareEnabled] = React.useState(false)
const onScreenShareChange = React.useCallback(
(enabled: boolean) => {
setIsScreenShareEnabled(enabled)
},
[setIsScreenShareEnabled]
)
const htmlProps = mergeProps({ className: 'lk-control-bar' }, props)
const {
@@ -125,71 +105,38 @@ export function ControlBar({
return (
<div {...htmlProps}>
<div className="lk-button-group">
<TrackToggle
source={Track.Source.Microphone}
showIcon={showIcon}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
>
{showText && t('controls.microphone')}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
kind="audioinput"
onActiveDeviceChange={(_kind, deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
</div>
</div>
<div className="lk-button-group">
<TrackToggle
source={Track.Source.Camera}
showIcon={showIcon}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
>
{showText && t('controls.camera')}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
kind="videoinput"
onActiveDeviceChange={(_kind, deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
</div>
</div>
<SelectToggleDevice
source={Track.Source.Microphone}
onChange={microphoneOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Microphone, error })
}
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
/>
<SelectToggleDevice
source={Track.Source.Camera}
onChange={cameraOnChange}
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.Camera, error })
}
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
/>
{browserSupportsScreenSharing && (
<TrackToggle
source={Track.Source.ScreenShare}
captureOptions={{ audio: true, selfBrowserSurface: 'include' }}
showIcon={showIcon}
onChange={onScreenShareChange}
<ScreenShareToggle
onDeviceError={(error) =>
onDeviceError?.({ source: Track.Source.ScreenShare, error })
}
>
{showText &&
t(
isScreenShareEnabled
? 'controls.stopScreenShare'
: 'controls.shareScreen'
)}
</TrackToggle>
/>
)}
<HandToggle />
<ChatToggle />
<ParticipantsToggle />
<OptionsButton />
<DisconnectButton>
{showIcon && <LeaveIcon />}
{showText && t('controls.leave')}
</DisconnectButton>
<LeaveButton />
<StartMediaButton />
</div>
)
@@ -30,11 +30,12 @@ import {
import { ControlBar } from './ControlBar'
import { styled } from '@/styled-system/jsx'
import { cva } from '@/styled-system/css'
import { ParticipantsList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsList'
import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants'
import { layoutStore } from '@/stores/layout'
import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
const LayoutWrapper = styled(
'div',
@@ -171,8 +172,8 @@ export function VideoConference({
])
/* eslint-enable react-hooks/exhaustive-deps */
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
return (
<div className="lk-video-conference" {...props}>
@@ -184,32 +185,45 @@ export function VideoConference({
>
<div className="lk-video-conference-inner">
<LayoutWrapper>
{!focusTrack ? (
<div
className="lk-grid-layout-wrapper"
style={{ height: 'auto' }}
>
<GridLayout tracks={tracks}>
<ParticipantTile />
</GridLayout>
</div>
) : (
<div className="lk-focus-layout-wrapper">
<FocusLayoutContainer>
<CarouselLayout tracks={carouselTracks}>
<div
style={{ display: 'flex', position: 'relative', width: '100%' }}
>
{!focusTrack ? (
<div
className="lk-grid-layout-wrapper"
style={{ height: 'auto' }}
>
<GridLayout tracks={tracks}>
<ParticipantTile />
</CarouselLayout>
{focusTrack && <FocusLayout trackRef={focusTrack} />}
</FocusLayoutContainer>
</div>
)}
</GridLayout>
</div>
) : (
<div
className="lk-focus-layout-wrapper"
style={{ height: 'auto' }}
>
<FocusLayoutContainer>
<CarouselLayout
tracks={carouselTracks}
style={{
minWidth: '200px',
}}
>
<ParticipantTile />
</CarouselLayout>
{focusTrack && <FocusLayout trackRef={focusTrack} />}
</FocusLayoutContainer>
</div>
)}
<MainNotificationToast />
</div>
<Chat
style={{ display: widgetState.showChat ? 'grid' : 'none' }}
messageFormatter={chatMessageFormatter}
messageEncoder={chatMessageEncoder}
messageDecoder={chatMessageDecoder}
/>
{showParticipants && <ParticipantsList />}
{sidePanel && <SidePanel />}
</LayoutWrapper>
<ControlBar />
</div>
@@ -1,7 +1,8 @@
import { Trans, useTranslation } from 'react-i18next'
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
import { A, Badge, Dialog, type DialogProps, Field, H, P } from '@/primitives'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
import { logoutUrl, useUser } from '@/features/auth'
import { ProConnectButton } from '@/components/ProConnectButton'
export type SettingsDialogProps = Pick<DialogProps, 'isOpen' | 'onOpenChange'>
@@ -28,9 +29,7 @@ export const SettingsDialog = (props: SettingsDialogProps) => {
) : (
<>
<P>{t('account.youAreNotLoggedIn')}</P>
<P>
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
</P>
<ProConnectButton />
</>
)}
<H lvl={2}>{t('language.heading')}</H>
@@ -12,6 +12,8 @@ import {
import { AccountTab } from './tabs/AccountTab'
import { GeneralTab } from '@/features/settings/components/tabs/GeneralTab.tsx'
import { AudioTab } from '@/features/settings/components/tabs/AudioTab.tsx'
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
import { useRef } from 'react'
const tabsStyle = css({
maxHeight: '40.625rem', // fixme size copied from meet settings modal
@@ -28,13 +30,13 @@ const tabListContainerStyle = css({
borderRight: '1px solid lightGray', // fixme poor color management
paddingY: '1rem',
paddingRight: '1.5rem',
flex: '0 0 16rem', // fixme size copied from meet settings modal
})
const tabPanelContainerStyle = css({
display: 'flex',
flexGrow: '1',
marginTop: '3.5rem',
minWidth: 0,
})
export type SettingsDialogExtended = Pick<
@@ -45,25 +47,39 @@ export type SettingsDialogExtended = Pick<
export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
// display only icon on small screen
const { t } = useTranslation('settings')
const dialogEl = useRef<HTMLDivElement>(null)
const { width } = useSize(dialogEl)
const isWideScreen = !width || width >= 800 // fixme - hardcoded 50rem in pixel
return (
<Dialog {...props} role="dialog" type="flex">
<Dialog innerRef={dialogEl} {...props} role="dialog" type="flex">
<Tabs orientation="vertical" className={tabsStyle}>
<div className={tabListContainerStyle}>
<Heading slot="title" level={1} className={text({ variant: 'h1' })}>
{t('dialog.heading')}
</Heading>
<div
className={tabListContainerStyle}
style={{
flex: isWideScreen ? '0 0 16rem' : undefined,
paddingTop: !isWideScreen ? '64px' : undefined,
paddingRight: !isWideScreen ? '1rem' : undefined,
}}
>
{isWideScreen && (
<Heading slot="title" level={1} className={text({ variant: 'h1' })}>
{t('dialog.heading')}
</Heading>
)}
<TabList border={false} aria-label="Chat log orientation example">
<Tab icon highlight id="1">
<RiAccountCircleLine />
{t('tabs.account')}
{isWideScreen && t('tabs.account')}
</Tab>
<Tab icon highlight id="2">
<RiSpeakerLine />
{t('tabs.audio')}
{isWideScreen && t('tabs.audio')}
</Tab>
<Tab icon highlight id="3">
<RiSettings3Line />
{t('tabs.general')}
{isWideScreen && t('tabs.general')}
</Tab>
</TabList>
</div>
@@ -4,11 +4,12 @@ import {
usePersistentUserChoices,
useRoomContext,
} from '@livekit/components-react'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
import { logoutUrl, useUser } from '@/features/auth'
import { css } from '@/styled-system/css'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import { HStack } from '@/styled-system/jsx'
import { useState } from 'react'
import { ProConnectButton } from '@/components/ProConnectButton'
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'>
@@ -58,9 +59,7 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
) : (
<>
<P>{t('account.youAreNotLoggedIn')}</P>
<P>
<A href={authUrl()}>{t('login', { ns: 'global' })}</A>
</P>
<ProConnectButton />
</>
)}
<HStack
@@ -1,10 +1,55 @@
import { DialogProps, Field, H } from '@/primitives'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import { useMediaDeviceSelect } from '@livekit/components-react'
import {
useIsSpeaking,
useMediaDeviceSelect,
useRoomContext,
} from '@livekit/components-react'
import { isSafari } from '@/utils/livekit'
import { useTranslation } from 'react-i18next'
import { SoundTester } from '@/components/SoundTester.tsx'
import { SoundTester } from '@/components/SoundTester'
import { HStack } from '@/styled-system/jsx'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import { ReactNode } from 'react'
type RowWrapperProps = {
heading: string
children: ReactNode[]
}
const RowWrapper = ({ heading, children }: RowWrapperProps) => {
return (
<>
<H lvl={2}>{heading}</H>
<HStack
gap={0}
style={{
flexWrap: 'wrap',
}}
>
<div
style={{
flex: '1 1 215px',
minWidth: 0,
}}
>
{children[0]}
</div>
<div
style={{
width: '10rem',
justifyContent: 'center',
display: 'flex',
paddingLeft: '1.5rem',
}}
>
{children[1]}
</div>
</HStack>
</>
)
}
export type AudioTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'>
@@ -13,6 +58,9 @@ type DeviceItems = Array<{ value: string; label: string }>
export const AudioTab = ({ id }: AudioTabProps) => {
const { t } = useTranslation('settings')
const { localParticipant } = useRoomContext()
const isSpeaking = useIsSpeaking(localParticipant)
const {
devices: devicesOut,
@@ -60,20 +108,32 @@ export const AudioTab = ({ id }: AudioTabProps) => {
return (
<TabPanel padding={'md'} flex id={id}>
<H lvl={2}>{t('audio.microphone.heading')}</H>
<Field
type="select"
label={t('audio.microphone.label')}
items={itemsIn}
defaultSelectedKey={activeDeviceIdIn || getDefaultSelectedKey(itemsIn)}
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
{...disabledProps}
/>
<RowWrapper heading={t('audio.microphone.heading')}>
<Field
type="select"
label={t('audio.microphone.label')}
items={itemsIn}
defaultSelectedKey={
activeDeviceIdIn || getDefaultSelectedKey(itemsIn)
}
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
{...disabledProps}
style={{
width: '100%',
}}
/>
<>
{localParticipant.isMicrophoneEnabled ? (
<ActiveSpeaker isSpeaking={isSpeaking} />
) : (
<span>Micro désactivé</span>
)}
</>
</RowWrapper>
{/* Safari has a known limitation where its implementation of 'enumerateDevices' does not include audio output devices.
To prevent errors or an empty selection list, we only render the speakers selection field on non-Safari browsers. */}
{!isSafari() && (
<>
<H lvl={2}>{t('audio.speakers.heading')}</H>
<RowWrapper heading={t('audio.speakers.heading')}>
<Field
type="select"
label={t('audio.speakers.label')}
@@ -85,9 +145,12 @@ export const AudioTab = ({ id }: AudioTabProps) => {
setActiveMediaDeviceOut(key as string)
}
{...disabledProps}
style={{
minWidth: 0,
}}
/>
<SoundTester />
</>
</RowWrapper>
)}
</TabPanel>
)
@@ -0,0 +1,31 @@
import { useEffect } from 'react'
import { Crisp } from 'crisp-sdk-web'
import { ApiUser } from '@/features/auth/api/ApiUser'
export const initializeSupportSession = (user: ApiUser) => {
if (!Crisp.isCrispInjected()) return
const { id, email } = user
Crisp.setTokenId(`meet-${id}`)
Crisp.user.setEmail(email)
}
export const terminateSupportSession = () => {
if (!Crisp.isCrispInjected()) return
Crisp.setTokenId()
Crisp.session.reset()
}
export type useSupportProps = {
id?: string
}
// Configure Crisp chat for real-time support across all pages.
export const useSupport = ({ id }: useSupportProps) => {
useEffect(() => {
if (!id || Crisp.isCrispInjected()) return
Crisp.configure(id)
Crisp.setHideOnMobile(true)
}, [id])
return null
}
+8 -3
View File
@@ -2,13 +2,16 @@ import { Link } from 'wouter'
import { css } from '@/styled-system/css'
import { Stack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { A, Text, Button } from '@/primitives'
import { Text, Button } from '@/primitives'
import { SettingsButton } from '@/features/settings'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
import { logoutUrl, useUser } from '@/features/auth'
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
import { Feedback } from '@/components/Feedback'
import { Menu } from '@/primitives/Menu'
import { MenuList } from '@/primitives/MenuList'
import { ProConnectButton } from '@/components/ProConnectButton'
import { terminateAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
import { terminateSupportSession } from '@/features/support/hooks/useSupport'
export const Header = () => {
const { t } = useTranslation()
@@ -63,7 +66,7 @@ export const Header = () => {
<nav>
<Stack gap={1} direction="row" align="center">
{isLoggedIn === false && !isHome && (
<A href={authUrl()}>{t('login')}</A>
<ProConnectButton hint={false} />
)}
{!!user && (
<Menu>
@@ -79,6 +82,8 @@ export const Header = () => {
items={[{ value: 'logout', label: t('logout') }]}
onAction={(value) => {
if (value === 'logout') {
terminateAnalyticsSession()
terminateSupportSession()
window.location.href = logoutUrl()
}
}}
+6 -2
View File
@@ -1,5 +1,5 @@
{
"app": "Meet",
"app": "Visio",
"backToHome": "",
"cancel": "",
"closeDialog": "",
@@ -12,7 +12,11 @@
},
"loading": "",
"loggedInUserTooltip": "",
"login": "Anmelden",
"login": {
"buttonLabel": "",
"linkLabel": "",
"link": ""
},
"logout": "",
"notFound": {
"heading": ""
+2 -2
View File
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "",
"description": "",
"copied": "",
"copy": "",
"inputLabel": ""
"copied": "",
"permissions": ""
}
}
@@ -0,0 +1,10 @@
{
"defaultName": "",
"joined": {
"description": ""
},
"raised": {
"description": "",
"cta": ""
}
}
+64 -7
View File
@@ -4,15 +4,32 @@
"heading": ""
},
"join": {
"camlabel": "",
"videoinput": {
"choose": "",
"disable": "",
"enable": "",
"label": "",
"placeholder": ""
},
"audioinput": {
"choose": "",
"disable": "",
"enable": "",
"label": ""
},
"heading": "",
"joinLabel": "",
"micLabel": "",
"userLabel": ""
"joinMeeting": "",
"toggleOff": "",
"toggleOn": "",
"userLabel": "",
"usernameHint": "",
"usernameLabel": ""
},
"leaveRoomPrompt": "",
"shareDialog": {
"copy": "",
"copied": "",
"heading": "",
"description": "",
"permissions": ""
@@ -32,6 +49,14 @@
"open": "",
"closed": ""
},
"hand": {
"raise": "",
"lower": ""
},
"screenShare": {
"start": "",
"stop": ""
},
"leave": "",
"participants": {
"open": "",
@@ -44,18 +69,50 @@
"feedbacks": "",
"support": "",
"settings": "",
"username": ""
"username": "",
"effects": ""
}
},
"participants": {
"effects": {
"activateCamera": "",
"notAvailable": "",
"heading": "",
"closeButton": "",
"blur": {
"light": "",
"normal": "",
"apply": "",
"clear": ""
},
"experimental": ""
},
"sidePanel": {
"heading": {
"participants": "",
"effects": ""
},
"content": {
"participants": "",
"effects": ""
},
"closeButton": ""
},
"participants": {
"subheading": "",
"contributors": "",
"collapsable": {
"open": "",
"close": ""
},
"you": "",
"muteYourself": "",
"muteParticipant": "",
"muteParticipantAlert": {
"description": "",
"confirm": "",
"cancel": ""
}
},
"raisedHands": "",
"lowerParticipantHand": "",
"lowerParticipantsHand": ""
}
}
+7 -3
View File
@@ -1,10 +1,10 @@
{
"app": "Meet",
"app": "Visio",
"backToHome": "Back to homescreen",
"cancel": "Cancel",
"closeDialog": "Close dialog",
"error": {
"heading": "An error occured while loading the page"
"heading": "An error occurred while loading the page"
},
"feedbackAlert": "Give us feedback",
"forbidden": {
@@ -12,7 +12,11 @@
},
"loading": "Loading…",
"loggedInUserTooltip": "Logged in as…",
"login": "Login",
"login": {
"buttonLabel": "Login with ProConnect",
"linkLabel": "What is ProConnect? - new tab",
"link": "What is ProConnect?"
},
"logout": "Logout",
"notFound": {
"heading": "Page not found"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"createMeeting": "Create a meeting",
"heading": "Welcome in Meet",
"heading": "Welcome in Visio",
"intro": "Work easily, from anywhere.",
"joinInputError": "Use a meeting link or code. Examples:",
"joinInputExample": "URL or 10-letter code",
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "Your connection details",
"description": "Send this link to the people you want to invite to the meeting. They will be able to join without Agent Connect.",
"copied": "Copied",
"copy": "Copy",
"inputLabel": "Meeting link"
"copy": "Copy the meeting link",
"copied": "Link copied to clipboard",
"permissions": "People with this link do not need your permission to join this meeting."
}
}
@@ -0,0 +1,10 @@
{
"defaultName": "A contributor",
"joined": {
"description": "{{name}} has joined the room"
},
"raised": {
"description": "{{name}} has raised their hand.",
"cta": "Open waiting list"
}
}
+68 -13
View File
@@ -1,18 +1,35 @@
{
"feedback": {
"body": "Please fill out the form available in the header to give us your precious feedback! Thanks.",
"heading": "Help us improve Meet"
"heading": "Help us improve Visio"
},
"join": {
"camlabel": "Camera",
"heading": "Join the meeting",
"videoinput": {
"choose": "Select camera",
"disable": "Disable camera",
"enable": "Enable camera",
"label": "Camera",
"placeholder": "Enable camera to see the preview"
},
"audioinput": {
"choose": "Select microphone",
"disable": "Disable microphone",
"enable": "Enable microphone",
"label": "Microphone"
},
"heading": "Verify your settings",
"joinLabel": "Join",
"micLabel": "Microphone",
"userLabel": "Your name"
"joinMeeting": "Join meeting",
"toggleOff": "Click to turn off",
"toggleOn": "Click to turn on",
"userLabel": "",
"usernameHint": "Shown to other participants",
"usernameLabel": "Your name"
},
"leaveRoomPrompt": "This will make you leave the meeting.",
"shareDialog": {
"copy": "Copy link",
"copy": "Copy the meeting link",
"copied": "Link copied to clipboard",
"heading": "Your meeting is ready",
"description": "Share this link with people you want to invite to the meeting.",
"permissions": "People with this link do not need your permission to join this meeting."
@@ -26,12 +43,18 @@
"controls": {
"microphone": "Microphone",
"camera": "Camera",
"shareScreen": "Share screen",
"stopScreenShare": "Stop screen share",
"chat": {
"open": "Close the chat",
"closed": "Open the chat"
},
"hand": {
"raise": "Raise hand",
"lower": "Lower hand"
},
"screenShare": {
"start": "Share screen",
"stop": "Stop screen share"
},
"leave": "Leave",
"participants": {
"open": "Hide everyone",
@@ -44,18 +67,50 @@
"feedbacks": "Give us feedbacks",
"support": "Get Help on Tchap",
"settings": "Settings",
"username": "Update Your Name"
"username": "Update Your Name",
"effects": "Apply effects"
}
},
"effects": {
"activateCamera": "Your camera is disabled. Choose an option to enable it.",
"notAvailable": "The blur effect will be available soon on your browser. We're working on it! In the meantime, you can use Google Chrome :(",
"heading": "Blur",
"blur": {
"light": "Light blur",
"normal": "Blur",
"apply": "Enable blur",
"clear": "Disable blur"
},
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."
},
"sidePanel": {
"heading": {
"participants": "Participants",
"effects": "Effects"
},
"content": {
"participants": "participants",
"effects": "effects"
},
"closeButton": "Hide {{content}}"
},
"participants": {
"heading": "Participants",
"closeButton": "Hide participants",
"subheading": "In room",
"you": "You",
"contributors": "Contributors",
"collapsable": {
"open": "Open {{name}} list",
"close": "Close {{name}} list"
},
"muteYourself": "Close your mic",
"muteParticipant": "Close the mic of {{name}}",
"muteParticipantAlert": {
"description": "Mute {{name}} for all participants? {{name}} is the only person who can unmute themselves.",
"description": "Mute {{name}} for all participants? {{name}} will be the only one who can unmute themselves.",
"confirm": "Mute",
"cancel": "Cancel"
}
},
"raisedHands": "Raised hands",
"lowerParticipantHand": "Lower {{name}}'s hand",
"lowerParticipantsHand": "Lower all hands"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"currentlyLoggedAs": "You are currently logged in as <0>{{user}}</0>",
"heading": "Account",
"youAreNotLoggedIn": "You are not logged in.",
"nameLabel": "Votre Nom",
"nameLabel": "Your Name",
"authentication": "Authentication"
},
"audio": {
+6 -2
View File
@@ -1,5 +1,5 @@
{
"app": "Meet",
"app": "Visio",
"backToHome": "Retour à l'accueil",
"cancel": "Annuler",
"closeDialog": "Fermer la fenêtre modale",
@@ -12,7 +12,11 @@
},
"loading": "Chargement…",
"loggedInUserTooltip": "Connecté en tant que…",
"login": "Se connecter",
"login": {
"buttonLabel": "S'identifier avec ProConnect",
"linkLabel": "Qu'est-ce que ProConnect ? - nouvelle fenêtre",
"link": "Qu'est-ce que ProConnect ?"
},
"logout": "Se déconnecter",
"notFound": {
"heading": "Page introuvable"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"createMeeting": "Créer une réunion",
"heading": "Meet",
"heading": "Visio",
"intro": "Collaborez en toute simplicité, où que vous soyez.",
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "Vos informations de connexion",
"description": "Envoyez ce lien aux personnes que vous souhaitez inviter à la réunion. Ils pourront la rejoindre sans Agent Connect.",
"copied": "Lien copié",
"copy": "Copier le lien",
"inputLabel": "Lien vers la réunion"
"copy": "Copier le lien de la réunion",
"copied": "Lien copié dans le presse-papiers",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion."
}
}
@@ -0,0 +1,10 @@
{
"defaultName": "Un contributeur",
"joined": {
"description": "{{name}} participe à l'appel"
},
"raised": {
"description": "{{name}} a levé la main.",
"cta": "Ouvrir la file d'attente"
}
}
+67 -12
View File
@@ -1,18 +1,35 @@
{
"feedback": {
"body": "Remplissez le formulaire disponible dans l'entête du site pour nous donner votre avis sur l'outil. Vos retours sont précieux ! Merci.",
"heading": "Aidez-nous à améliorer Meet"
"heading": "Aidez-nous à améliorer Visio"
},
"join": {
"camlabel": "Webcam",
"heading": "Rejoindre la réunion",
"videoinput": {
"choose": "Choisir la webcam",
"disable": "Désactiver la webcam",
"enable": "Activer la webcam",
"label": "Webcam",
"placeholder": "Activez la webcam pour prévisualiser l'affichage"
},
"audioinput": {
"choose": "Choisir le micro",
"disable": "Désactiver le micro",
"enable": "Activer le micro",
"label": "Microphone"
},
"heading": "Vérifiez vos paramètres",
"joinLabel": "Rejoindre",
"micLabel": "Micro",
"userLabel": "Votre nom"
"joinMeeting": "Rejoindre la réjoindre",
"toggleOff": "Cliquez pour désactiver",
"toggleOn": "Cliquez pour activer",
"userLabel": "",
"usernameHint": "Affiché aux autres participants",
"usernameLabel": "Votre nom"
},
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
"shareDialog": {
"copy": "Copier le lien",
"copy": "Copier le lien de la réunion",
"copied": "Lien copié dans le presse-papiers",
"heading": "Votre réunion est prête",
"description": "Partagez ce lien avec les personnes que vous souhaitez inviter à la réunion.",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion."
@@ -26,12 +43,18 @@
"controls": {
"microphone": "Microphone",
"camera": "Camera",
"shareScreen": "Partager l'écran",
"stopScreenShare": "Arrêter le partage",
"chat": {
"open": "Masquer le chat",
"closed": "Afficher le chat"
},
"hand": {
"raise": "Lever la main",
"lower": "Baisser la main"
},
"screenShare": {
"start": "Partager l'écran",
"stop": "Arrêter le partage"
},
"leave": "Quitter",
"participants": {
"open": "Masquer les participants",
@@ -44,18 +67,50 @@
"feedbacks": "Partager votre avis",
"support": "Obtenir de l'aide sur Tchap",
"settings": "Paramètres",
"username": "Choisir votre nom"
"username": "Choisir votre nom",
"effects": "Appliquer des effets"
}
},
"effects": {
"activateCamera": "Votre camera est désactivée. Choisissez une option pour l'activer.",
"notAvailable": "L'effet de flou sera bientôt disponible sur votre navigateur. Nous y travaillons ! En attendant, vous pouvez utiliser Google Chrome :(",
"heading": "Flou",
"blur": {
"light": "Léger flou",
"normal": "Flou",
"apply": "Appliquer le flou",
"clear": "Désactiver le flou"
},
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet sur tous les navigateurs et une meilleur qualité."
},
"sidePanel": {
"heading": {
"participants": "Participants",
"effects": "Effets"
},
"content": {
"participants": "les participants",
"effects": "les effets"
},
"closeButton": "Masquer {{content}}"
},
"participants": {
"heading": "Participants",
"closeButton": "Masquer les participants",
"subheading": "Dans la réunion",
"you": "Vous",
"contributors": "Contributeurs",
"collapsable": {
"open": "Ouvrir la liste {{name}}",
"close": "Fermer la liste {{name}}"
},
"muteYourself": "Couper votre micro",
"muteParticipant": "Couper le micro de {{name}}",
"muteParticipantAlert": {
"description": "Couper le micro de {{name}} pour tous les participants ? {{name}} est la seule personne habilitée à réactiver son micro",
"confirm": "Couper le micro",
"cancel": "Annuler"
}
},
"raisedHands": "Mains levées",
"lowerParticipantHand": "Baisser la main de {{name}}",
"lowerParticipantsHand": "Baisser la main de tous les participants"
}
}
+5 -4
View File
@@ -134,10 +134,11 @@ export const Checkbox = ({
>
<StyledCheckbox {...props}>
{(renderProps) => {
renderProps.isInvalid && !!props.validate
? setError(props.validate(renderProps.isSelected))
: setError(null)
if (renderProps.isInvalid && !!props.validate) {
setError(props.validate(renderProps.isSelected))
} else {
setError(null)
}
return (
<>
<div className="mt-Checkbox-checkbox">
+4 -1
View File
@@ -10,6 +10,7 @@ import {
} from 'react-aria-components'
import { Div, Button, Box, VerticallyOffCenter } from '@/primitives'
import { text } from './Text'
import { MutableRefObject } from 'react'
const StyledModalOverlay = styled(ModalOverlay, {
base: {
@@ -61,6 +62,7 @@ export type DialogProps = RACDialogProps & {
*/
onOpenChange?: (isOpen: boolean) => void
type?: 'flex'
innerRef?: MutableRefObject<HTMLDivElement | null>
}
export const Dialog = ({
@@ -69,6 +71,7 @@ export const Dialog = ({
onClose,
isOpen,
onOpenChange,
innerRef,
...dialogProps
}: DialogProps) => {
const isAlert = dialogProps['role'] === 'alertdialog'
@@ -93,7 +96,7 @@ export const Dialog = ({
<VerticallyOffCenter>
<Div margin="auto" width="fit-content" maxWidth="full">
<Div margin="1rem" pointerEvents="auto">
<Box size="sm" type={boxType}>
<Box size="sm" type={boxType} ref={innerRef}>
{!!title && (
<Heading
slot="title"
+1
View File
@@ -23,6 +23,7 @@ import { Div } from './Div'
const FieldWrapper = styled('div', {
base: {
marginBottom: 'textfield',
minWidth: 0,
},
})
+1 -1
View File
@@ -26,7 +26,7 @@ export const MenuList = <T extends string | number = string>({
const label = typeof item === 'string' ? item : item.label
return (
<MenuItem
className={menuItemRecipe()}
className={menuItemRecipe({ extraPadding: true })}
key={value}
id={value as string}
onAction={() => {
+3
View File
@@ -43,6 +43,9 @@ const StyledButton = styled(Button, {
const StyledSelectValue = styled(SelectValue, {
base: {
textOverflow: 'ellipsis',
overflow: 'hidden',
textWrap: 'nowrap',
'&[data-placeholder]': {
color: 'default.subtle-text',
fontStyle: 'italic',
+10
View File
@@ -0,0 +1,10 @@
import { styled } from '@/styled-system/jsx'
import { Separator as RACSeparator } from 'react-aria-components'
export const Separator = styled(RACSeparator, {
base: {
height: '1px',
background: 'gray.400',
margin: '4px 0',
},
})
@@ -25,7 +25,7 @@ export const TooltipWrapper = ({
children: ReactNode
} & TooltipWrapperProps) => {
return tooltip ? (
<TooltipTrigger delay={tooltipType === 'instant' ? 300 : 1000}>
<TooltipTrigger delay={tooltipType === 'instant' ? 150 : 1000}>
{children}
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
+16 -3
View File
@@ -23,6 +23,9 @@ export const buttonRecipe = cva({
'&[data-selected]': {
backgroundColor: 'colorPalette.active',
},
'&[data-disabled]': {
cursor: 'auto',
},
},
variants: {
size: {
@@ -70,6 +73,13 @@ export const buttonRecipe = cva({
backgroundColor: 'success.subtle!',
},
},
text: {
color: 'primary',
'&[data-hovered]': {
background: 'gray.100 !important',
color: 'primary !important',
},
},
danger: {
colorPalette: 'danger',
borderColor: 'danger.600',
@@ -140,6 +150,7 @@ export const buttonRecipe = cva({
legacyStyle: {
true: {
borderColor: 'gray.400',
transition: 'border 200ms, background 200ms, color 200ms',
'&[data-hovered]': {
borderColor: 'gray.500',
},
@@ -147,10 +158,12 @@ export const buttonRecipe = cva({
borderColor: 'gray.500',
},
'&[data-selected]': {
background: '#e5e7eb',
borderColor: 'gray.400',
backgroundColor: '#1d4ed8',
color: 'white',
borderColor: 'gray.500',
'&[data-hovered]': {
backgroundColor: 'gray.300',
borderColor: '#6b7280',
backgroundColor: '#1e40af',
},
},
},
+2
View File
@@ -2,8 +2,10 @@ import { proxy } from 'valtio'
type State = {
showHeader: boolean
sidePanel: 'participants' | 'effects' | null
}
export const layoutStore = proxy<State>({
showHeader: false,
sidePanel: null,
})
-9
View File
@@ -1,9 +0,0 @@
import { proxy } from 'valtio'
type State = {
showParticipants: boolean
}
export const participantsStore = proxy<State>({
showParticipants: false,
})
+44 -35
View File
@@ -18,65 +18,74 @@ sops:
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwZG8rTjAzcVNtVHdLZmJI
Mm4vemNrV1dDUExEZ1R1UGJmWjN3aWw1VXc0CjF1MVhhUTV6ckl1OEFNSjBJUCta
MTE3QU04RDJKMWlWcHhDSG1NTmZyTkEKLS0tIHh5UHRqckUxZWZLUDl4d3FDdHJs
Y0VMc0llMytmMWNUOW56d3ltTHpwZ2cKEHkn6wuHNeMTk+E6nEMpEJZ6wpdXSi3k
FkzXRa6SudAgA4R6K9EieHKPdiNvi0IsCJOhLpiNQtENu9poF5ozqQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCQnRJY0xWNFVRYkR0QTND
Rkc5dDRITUpRMW1EVlhEUk9xN0ttbElrdEJvClBMVnVKMXhGMThTcTVaN0s3OG1v
S0U2UUNYZ3FzcXUwVXRyS1hPZkpxcmsKLS0tIHhWei9KVmxFaXgzRUxmMWU2RmhG
Y1ZQekpQS3poSWxHZXUvbitlc2lIczAKjLZlgUSz51W4GHirUn352eFPSxIK1/Wf
N+kzoUvMLmwwfHDztFFYLOEE9x1zx7GoPGG7fN9bTG7GWgcRdd7NUw==
-----END AGE ENCRYPTED FILE-----
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAva3lFTTM2ZUJpcnozNmJt
ZHAvRndkSjRCVEZYN29wMWFZVmV5ekNiYW5FCkI3RWUvVTdVZ2dJeWVSY085SW5G
UWNOempRQlFCUUlmL0RIR1ZZZVI5cDQKLS0tIEpwZkVodENLM1VnQ1o1TmhRSFFi
ZEF5MUNVdHRKcW9aK0M2M0ZVNDBxbjgKw8Wp06PcoStrO6ppsOR5zWjXbYrP64Dv
XAEHQMa7vyvuu12YIa/fpyDM4HrsrPq3OWudxuWS6p0X8xmPYrXm6g==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3aTVDMDNUUFFVQlE3Ykpl
ZlFQUHQ0UUdlTU54WlEvanBWeGVlYnd1aEZFClQ3Y1ZSOHp6QUt1UXBLV3NWaGJ4
T2ZwKzZXcXh1Z2llQk9SN1RRLzNOdGcKLS0tIHN0YTFpL05SQUc1WTRsQ2Z0ZGtu
ZjhoUHhFbkMvRUhscy9LY05xbXFZVlEKXrt6UPLOprcnsTFX1WCF/pIWXmjiPGEx
7NNbnAl+A9yfheCeiJdWcj5ZBCa6Nx2udVMNjQ7ITMzhIY4BBSicuQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1OU9mZHZFanpTQjE3aGly
VVZDWFh5ZmRHV1YwWUtsZVd6R090SnkxVVNVCmM1UWwwaFlqTHlhSGFRblZBbVhl
aEhtWWp1N1lVTDNqOFJBL0swVFl3Y3cKLS0tIG5ZTHAveFdLdGhQeUc2Tzh1d0dw
UzRhNGNTZFR3U3BLNFhCdFp6aU5uZVEKL4K5jFjPfMp/fMA8+nQerj6PE5zvGeHW
1SuHwnDiglKmksj8Gy+7spwLQCmo11JCnW2gXKktVIe5XOyhortq3Q==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1Y3JyU3JPOWdiamd0cEFV
UWRUdUQ5V3ZnQXQ0YURLZ3RiY0toN29vNkhJCnIzYWdkV3puVWQ5Nm5SVGpobllB
OW5WWkVLaE55eUs2OWNDWkZiOGNuZW8KLS0tIHN3eGVST1BRMU1JRWNRZGNJeGdi
SWJXQ2VMZ3kyWElBSjBKc0tQWmIwMjgKLDakU3iHwVTQBYGR0d2TFFdLdsct49y1
/S8vydlcx08L0yWHbfamhiYJE3BZbRXZue6z5irBPKEVjGD42aSREw==
-----END AGE ENCRYPTED FILE-----
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Z1lXM0tDRzJnMytDZHBV
ZlpCcTg0U3RGeEVzN2dBMkFYa0FBMFVKMWw4Cm5KeWMvdFBRNHFJWFNSS3NVWlNQ
dnhkc2F2MFVTMmVHT2U2STdUWWcvNzAKLS0tIHkzQXltQW5TWHVrektQU1hES1ll
UnVld1laTWRmTlkzWHRKRjJmWVJhbU0KANxoSnhDbxsja+Eo8MVCGv6iThNmlo/m
y0RXVNVqNlqTreT0F/SKmP74W3lF30nwsfrOywMQyu75k8p5MGwUuQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKUklKTzRzdHZiZjdHSmY2
WCs3ZFVodFBGcDdvZWZaaHNnbG9mdFk2WVJjCjUwZjA2Qi9CR1JYdjJ3VFlhZDZR
REEyZStrTjIwbWdCdVU5NWo2Y2hBemsKLS0tIFZmWVh0Z1BFUGRjZExMNlNJSFVJ
VnlWY3U4dW4ycXJCOVVUUlJxYzVONGsKy47vHe+awhJyI/pSUOhvUiOt+jtn+Vwg
Rlm93bJr0GNVWYm521r/I409s3TMitQerweGnl3u7t1FaKX7oI1qBQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwUm9oT3RYbGZXaW5CM20r
QlYyeGZjMG05MFNzdndCU2dUemZqdXF5Um1FCkZWdVlubW1MZ3NwM3V2S3dQRFA1
cHVvOXBMMGtMdkF4T2s3Wk9wYkJUZUEKLS0tIFBRM3FWYTRrVDNwZStwQWlpbEYx
dXA5WE5udnhhMVdLTk1jMzJuU3VWaTQKaxTpyqi5fjmOFR4qOxm+wSqWDxb/96QI
rHGUI+CqMVKZ6w1fMH0uanvCuw7Q9rbmsKB+DsjARMLFGqxzP/psUA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVUE9nVk1SMUp5WW9zYnZs
OFBYWmpwZW56S0J0Qk43dmlCWG5RV0RIKzM0CkVNbzI5VSs3VVdaT2xnVEhGdW9h
QW02K0ZmeGxsSDZMTGdETjkwd2xVd2MKLS0tIHp2WWQyUFphMmlZaDBZR0JQZ095
UTVaYXl6SkZUNElUdUlZcU8rditFd3cK0CqMiDzEItfB/T0K8YEncp9HuKWVTy7q
2LgHBQJi35sdBviEpsHZt7BlHTKanbmB5S9oUexNq+3wUP9e1n5CGA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwMmVOaHV1Unl4UmUrVXEy
REJIYWZhQmt3R0R5RFRveTl4Z0JaZlpKNmd3Ckl0dkFUaG90TE5odVh0YVprMVZj
T2tXS0dSRGVVczdhb1dlVlV0L3JZUncKLS0tIGo2RDBPZXNzU2dQQlJhY0NBS2ps
RW92OFVTS3d2L3UxOXNrVUFSVFI5TXMKn3pdHbXxBccG6Q+gWPVQK/5wiIKkzdhm
HiNzezStrkFHf/lsFS2LNgYkfMMzBQ4rJj7oQkrD1Z8j6qRld3zzLg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0VDdRaEVCemd5T0ozSmtp
NllJa0tURVhxTXlZQS8vNUZxbGFQVnhRMDJjClhlaE53WTRpeEtVSGErYUdyZk1h
SloxZGNUdFRDZ1J4bzdneFUyNXdXdkUKLS0tIGdxU3RCUEVmcTdnZnFidkROdXJS
dVdQUUdVUUhpVVh6SytRV1V0RHV6cDQKagJA6w43n8yh119PrJltPPh/EbHIKjfH
G8RfgXBrf5iWdWsyD8haDL6WsjdbVQsbPCz+ucULfnZ1Dn8A/3yQUQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age18fgn6j2vwwswqcpv9xpcehq8mrf9zs2sglwkamp3tzwx8d9jq9jsrskrk9
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzeWN4ZE5Ya3RPV2VKRFdv
UGQ4QTJUZFovSXpQREhNWXdQbktVUHFQaUhjCmM1dXNxbGdlVjBWNjc5L2E5QlBy
NjI0S251cjZvYU1nQ2swWXVDdjdhSG8KLS0tIHdFNXptN1VBNytFNW40MUsxTGFM
WlVnc3VLY1ZReDFxUVlqWU9rM0xMQ00Kq/ckdP1N4BDBo0pSH9pp36skIacwNm3d
utiuCgG16Hqe6YDb3agx81BocuSJ9oMOdpegoztnkkBDDP0F+e/VuQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIbGgxRG5sZHFPZm5OTnZw
L21JUnFDOTNZUitYNHJad3FnYkk2aDlQRzM0CnI1NXZDcEtQdjBWaVhJMElIOEZU
KzFjT0p4OVpzUFlnR1Q5cm5SVnhGaEUKLS0tIFliVVQ4VFg4QVJxazdyZG9PbE9M
M3NzZFRKMFkxZHZ1Rmh0VWFWMDZTQlEKxKVEvnnV56t/RSvP94TgjW1Do68Tn2N2
tJMI8VIp7rs5vxaToois6CMuhVONsl1CBozAEV8pqS5O830RGa1eog==
-----END AGE ENCRYPTED FILE-----
- recipient: age1hm2hsfgjezpsc3k0y5w5feq9t8vl3seq04qjhgt6ztd6403wfvpsgxu09m
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBta3VwOGQrSHdENGZLZUpq
SGw0Z2YwYm8vZ0h4Y01UclIrQlRVNWVJaHlRCmFMQkp1aGc5R1JrMVdPNmwvbkYv
NkxOc3pYY2E5dVVFejZJaitSb0hRUVUKLS0tIGFzclh3VXhBc3NYQjZlUmNUSk10
S2xqMEpBcEpaOHdBNUtvOWxsVUhLLzQKGT3grtnocBkaCKXbtH7cZu/ZP9MKsAjt
ZNmS1GahpBx2lVEbfJNLfwId+IJ6kOIyHj42g9yIQFfKTPKE32Ht8A==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-07-18T13:08:42Z"
mac: ENC[AES256_GCM,data:a/uHyw9V/SMIePV9nPf+wJgPg+YDYLJGYy7NMLBrBgCXtBWHHonSNjzdmtjix1bW2y+cU0gMqodrtqR1cJGBmXr4NRY7NJqgLWE9rEdYfG7BnfqsWmvAaTIrSs7QMZWkEic7ys/bXoA5BZoau3olhVqIO2A/iyBtoMU9Hv7hPlo=,iv:gaqSCUbN7cxWPNrFPDTl7xNxpOZL6GY/swD/MDCiRqk=,tag:Oz0f/DyD3KGV/9Rprj/1Xw==,type:str]
@@ -49,6 +49,8 @@ backend:
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
ANALYTICS_KEY: xwhoIMCZ8PBRjQ2t
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
migrate:
@@ -95,3 +97,11 @@ ingress:
ingressAdmin:
enabled: true
host: meet.127.0.0.1.nip.io
posthog:
ingress:
enabled: false
ingressAssets:
enabled: false
@@ -1,127 +0,0 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.4"
backend:
migrateJobAnnotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
envVars:
DJANGO_CSRF_TRUSTED_ORIGINS: http://meet-preprod.beta.numerique.gouv.fr,https://meet-preprod.beta.numerique.gouv.fr
DJANGO_CONFIGURATION: Production
DJANGO_ALLOWED_HOSTS: meet-preprod.beta.numerique.gouv.fr
DJANGO_SUPERUSER_EMAIL:
secretKeyRef:
name: backend
key: DJANGO_SUPERUSER_EMAIL
DJANGO_SECRET_KEY:
secretKeyRef:
name: backend
key: DJANGO_SECRET_KEY
DJANGO_SETTINGS_MODULE: meet.settings
DJANGO_SILENCED_SYSTEM_CHECKS: security.W004, security.W008
DJANGO_SUPERUSER_PASSWORD:
secretKeyRef:
name: backend
key: DJANGO_SUPERUSER_PASSWORD
DJANGO_EMAIL_HOST: "snap-mail.numerique.gouv.fr"
DJANGO_EMAIL_PORT: 465
DJANGO_EMAIL_USE_SSL: True
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
OIDC_OP_USER_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
OIDC_RP_CLIENT_ID:
secretKeyRef:
name: backend
key: OIDC_RP_CLIENT_ID
OIDC_RP_CLIENT_SECRET:
secretKeyRef:
name: backend
key: OIDC_RP_CLIENT_SECRET
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet-preprod.beta.numerique.gouv.fr
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://meet-preprod.beta.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://meet-preprod.beta.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://meet-preprod.beta.numerique.gouv.fr
DB_HOST:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: host
DB_NAME:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: database
DB_USER:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: username
DB_PASSWORD:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: password
DB_PORT:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: port
POSTGRES_USER:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: username
POSTGRES_DB:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: database
POSTGRES_PASSWORD:
secretKeyRef:
name: postgresql.postgres.libre.sh
key: password
REDIS_URL:
secretKeyRef:
name: redis.redis.libre.sh
key: url
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
LIVEKIT_API_SECRET:
secretKeyRef:
name: backend
key: LIVEKIT_API_SECRET
LIVEKIT_API_KEY:
secretKeyRef:
name: backend
key: LIVEKIT_API_KEY
LIVEKIT_API_URL: https://livekit-preprod.beta.numerique.gouv.fr
ALLOW_UNREGISTERED_ROOMS: False
createsuperuser:
command:
- "/bin/sh"
- "-c"
- |
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
restartPolicy: Never
frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.4"
ingress:
enabled: true
host: meet-preprod.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
ingressAdmin:
enabled: true
host: meet-preprod.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/start
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/auth
@@ -1,16 +1,16 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.4"
tag: "v0.1.6"
backend:
migrateJobAnnotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
envVars:
DJANGO_CSRF_TRUSTED_ORIGINS: https://meet.numerique.gouv.fr
DJANGO_CSRF_TRUSTED_ORIGINS: https://visio.numerique.gouv.fr,https://meet.numerique.gouv.fr
DJANGO_CONFIGURATION: Production
DJANGO_ALLOWED_HOSTS: meet.numerique.gouv.fr
DJANGO_ALLOWED_HOSTS: visio.numerique.gouv.fr,meet.numerique.gouv.fr
DJANGO_SECRET_KEY:
secretKeyRef:
name: backend
@@ -43,11 +43,11 @@ backend:
key: OIDC_RP_CLIENT_SECRET
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet.numerique.gouv.fr
OIDC_REDIRECT_ALLOWED_HOSTS: https://visio.numerique.gouv.fr
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://meet.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://meet.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://meet.numerique.gouv.fr
LOGIN_REDIRECT_URL: https://visio.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://visio.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://visio.numerique.gouv.fr
DB_HOST:
secretKeyRef:
name: postgresql.postgres.libre.sh
@@ -96,6 +96,9 @@ backend:
LIVEKIT_API_URL: https://livekit-preprod.beta.numerique.gouv.fr
ANALYTICS_KEY: mwuxTKi8o2xzWH54
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
FRONTEND_ANALYTICS: "{'id': 'phc_RPYko028Oqtj0c9exLIWwrlrjLxSdxT0ntW0Lam4iom', 'host': 'https://product.visio.numerique.gouv.fr'}"
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
createsuperuser:
command:
@@ -109,20 +112,39 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.4"
tag: "v0.1.6"
ingress:
enabled: true
host: meet.numerique.gouv.fr
host: visio.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
ingressAdmin:
enabled: true
host: meet.numerique.gouv.fr
host: visio.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/start
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy.beta.numerique.gouv.fr/oauth2/auth
posthog:
ingress:
enabled: true
host: product.visio.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/upstream-vhost: eu.i.posthog.com
nginx.ingress.kubernetes.io/backend-protocol: https
ingressAssets:
enabled: true
host: product.visio.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
nginx.ingress.kubernetes.io/upstream-vhost: eu-assets.i.posthog.com
nginx.ingress.kubernetes.io/backend-protocol: https
+41 -8
View File
@@ -8,9 +8,9 @@ backend:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
envVars:
DJANGO_CSRF_TRUSTED_ORIGINS: http://meet-staging.beta.numerique.gouv.fr,https://meet-staging.beta.numerique.gouv.fr
DJANGO_CSRF_TRUSTED_ORIGINS: http://visio-staging.beta.numerique.gouv.fr,https://meet-staging.beta.numerique.gouv.fr
DJANGO_CONFIGURATION: Production
DJANGO_ALLOWED_HOSTS: meet-staging.beta.numerique.gouv.fr
DJANGO_ALLOWED_HOSTS: visio-staging.beta.numerique.gouv.fr
DJANGO_SECRET_KEY:
secretKeyRef:
name: backend
@@ -42,11 +42,11 @@ backend:
key: OIDC_RP_CLIENT_SECRET
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet-staging.beta.numerique.gouv.fr
OIDC_REDIRECT_ALLOWED_HOSTS: https://visio-staging.beta.numerique.gouv.fr
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://meet-staging.beta.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://meet-staging.beta.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://meet-staging.beta.numerique.gouv.fr
LOGIN_REDIRECT_URL: https://visio-staging.beta.numerique.gouv.fr
LOGIN_REDIRECT_URL_FAILURE: https://visio-staging.beta.numerique.gouv.fr
LOGOUT_REDIRECT_URL: https://visio-staging.beta.numerique.gouv.fr
DB_HOST:
secretKeyRef:
name: postgresql.postgres.libre.sh
@@ -95,6 +95,8 @@ backend:
LIVEKIT_API_URL: https://livekit-staging.beta.numerique.gouv.fr
ANALYTICS_KEY: Roi1k6IAc2DEqHB0
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_ANALYTICS: "{'id': 'phc_RPYko028Oqtj0c9exLIWwrlrjLxSdxT0ntW0Lam4iom', 'host': 'https://product.visio-staging.beta.numerique.gouv.fr'}"
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
createsuperuser:
command:
@@ -112,16 +114,47 @@ frontend:
ingress:
enabled: true
host: meet-staging.beta.numerique.gouv.fr
host: visio-staging.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
enabled: true
additional:
- secretName: transitional-tls
hosts:
- {{ .Values.newDomain }}
ingressAdmin:
enabled: true
host: meet-staging.beta.numerique.gouv.fr
host: visio-staging.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/auth-signin: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/start
nginx.ingress.kubernetes.io/auth-url: https://oauth2-proxy-preprod.beta.numerique.gouv.fr/oauth2/auth
tls:
enabled: true
additional:
- secretName: transitional-tls
hosts:
- {{ .Values.newDomain }}
posthog:
ingress:
enabled: true
host: product.visio-staging.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/upstream-vhost: eu.i.posthog.com
nginx.ingress.kubernetes.io/backend-protocol: https
ingressAssets:
enabled: true
host: product.visio-staging.beta.numerique.gouv.fr
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/upstream-vhost: eu-assets.i.posthog.com
nginx.ingress.kubernetes.io/backend-protocol: https
+55
View File
@@ -0,0 +1,55 @@
{{ if .Values.addRedirect }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
{{ if .Values.enablePermanentRedirect }}
nginx.ingress.kubernetes.io/permanent-redirect: "https://{{ .Values.newDomain }}$request_uri"
nginx.ingress.kubernetes.io/permanent-redirect-code: "308"
{{ end }}
name: temporary-redirect
namespace: {{ .Release.Namespace | quote }}
spec:
ingressClassName: nginx
rules:
- host: {{ .Values.oldDomain }}
http:
paths:
- backend:
service:
name: meet-frontend
port:
number: 80
path: /
pathType: Prefix
- backend:
service:
name: meet-backend
port:
number: 80
path: /api
pathType: Prefix
tls:
- hosts:
- {{ .Values.oldDomain }}
secretName: transitional-tls
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: transitional-tls
namespace: {{ .Release.Namespace | quote }}
spec:
dnsNames:
- {{ .Values.newDomain }}
- {{ .Values.oldDomain }}
issuerRef:
group: cert-manager.io
kind: ClusterIssuer
name: {{ index .Values.ingress.annotations "cert-manager.io/cluster-issuer" }}
secretName: transitional-tls
usages:
- digital signature
- key encipherment
{{ end }}
+31 -22
View File
@@ -1,3 +1,28 @@
environments:
dev:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
staging:
values:
- version: 0.0.1
addRedirect: True
enablePermanentRedirect: True
oldDomain: meet-staging.beta.numerique.gouv.fr
newDomain: visio-staging.beta.numerique.gouv.fr
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
production:
values:
- version: 0.0.1
addRedirect: True
enablePermanentRedirect: True
oldDomain: meet.numerique.gouv.fr
newDomain: visio.numerique.gouv.fr
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
repositories:
- name: bitnami
url: registry-1.docker.io/bitnamicharts
@@ -36,6 +61,12 @@ releases:
chart: ./extra
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
values:
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
- addRedirect: {{ .Values | get "addRedirect" "False" }}
enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
newDomain: {{ .Values | get "newDomain" "demo.com" }}
- name: meet
version: {{ .Values.version }}
@@ -54,25 +85,3 @@ releases:
- env.d/{{ .Environment.Name }}/values.livekit.yaml.gotmpl
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
environments:
dev:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
staging:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
preprod:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
production:
values:
- version: 0.0.1
secrets:
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
+9
View File
@@ -157,6 +157,15 @@ Requires top level scope
{{ include "meet.fullname" . }}-webrtc
{{- end }}
{{/*
Full name for the Posthog
Requires top level scope
*/}}
{{- define "meet.posthog.fullname" -}}
{{ include "meet.fullname" . }}-posthog
{{- end }}
{{/*
Usage : {{ include "meet.secret.dockerconfigjson.name" (dict "fullname" (include "meet.fullname" .) "imageCredentials" .Values.path.to.the.image1) }}
*/}}
+2 -1
View File
@@ -6,8 +6,9 @@ kind: Job
metadata:
name: {{ $fullName }}-migrate
namespace: {{ .Release.Namespace | quote }}
{{- with .Values.backend.migrateJobAnnotations }}
annotations:
argocd.argoproj.io/sync-options: Replace=true,Force=true
{{- with .Values.backend.migrateJobAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
@@ -6,8 +6,9 @@ kind: Job
metadata:
name: {{ $fullName }}-createsuperuser
namespace: {{ .Release.Namespace | quote }}
{{- with .Values.backend.migrateJobAnnotations }}
annotations:
argocd.argoproj.io/sync-options: Replace=true,Force=true
{{- with .Values.backend.migrateJobAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
@@ -0,0 +1,87 @@
{{- if .Values.posthog.ingress.enabled -}}
{{- $fullName := include "meet.fullname" . -}}
{{- if and .Values.posthog.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.posthog.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.posthog.ingress.annotations "kubernetes.io/ingress.class" .Values.posthog.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}-posthog
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "meet.labels" . | nindent 4 }}
{{- with .Values.posthog.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.posthog.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.posthog.ingress.className }}
{{- end }}
{{- if .Values.posthog.ingress.tls.enabled }}
tls:
{{- if .Values.posthog.ingress.host }}
- secretName: {{ $fullName }}-posthog-tls
hosts:
- {{ .Values.posthog.ingress.host | quote }}
{{- end }}
{{- range .Values.posthog.ingress.tls.additional }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- if .Values.posthog.ingress.host }}
- host: {{ .Values.posthog.ingress.host | quote }}
http:
paths:
- path: {{ .Values.posthog.ingress.path }}
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ include "meet.posthog.fullname" . }}-proxy
port:
number: {{ .Values.posthog.service.port }}
{{- else }}
serviceName: {{ include "meet.posthog.fullname" . }}-proxy
servicePort: {{ .Values.posthog.service.port }}
{{- end }}
{{- end }}
{{- range .Values.posthog.ingress.hosts }}
- host: {{ . | quote }}
http:
paths:
- path: {{ $.Values.posthog.ingress.path | quote }}
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ include "meet.posthog.fullname" . }}-proxy
port:
number: {{ $.Values.posthog.service.port }}
{{- else }}
serviceName: {{ include "meet.posthog.fullname" . }}-proxy
servicePort: {{ $.Values.posthog.service.port }}
{{- end }}
{{- with $.Values.posthog.assetsService.customBackends }}
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
{{- end }}

Some files were not shown because too many files have changed in this diff Show More