Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d6589e487 |
@@ -22,19 +22,7 @@ class UserAdmin(auth_admin.UserAdmin):
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
_("Personal info"),
|
||||
{
|
||||
"fields": (
|
||||
"sub",
|
||||
"email",
|
||||
"full_name",
|
||||
"short_name",
|
||||
"language",
|
||||
"timezone",
|
||||
)
|
||||
},
|
||||
),
|
||||
(_("Personal info"), {"fields": ("sub", "email", "language", "timezone")}),
|
||||
(
|
||||
_("Permissions"),
|
||||
{
|
||||
@@ -64,8 +52,6 @@ class UserAdmin(auth_admin.UserAdmin):
|
||||
"sub",
|
||||
"admin_email",
|
||||
"email",
|
||||
"full_name",
|
||||
"short_name",
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
@@ -74,24 +60,9 @@ class UserAdmin(auth_admin.UserAdmin):
|
||||
"updated_at",
|
||||
)
|
||||
list_filter = ("is_staff", "is_superuser", "is_device", "is_active")
|
||||
ordering = (
|
||||
"is_active",
|
||||
"-is_superuser",
|
||||
"-is_staff",
|
||||
"-is_device",
|
||||
"-updated_at",
|
||||
"full_name",
|
||||
)
|
||||
readonly_fields = (
|
||||
"id",
|
||||
"sub",
|
||||
"email",
|
||||
"full_name",
|
||||
"short_name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
search_fields = ("id", "sub", "admin_email", "email", "full_name")
|
||||
ordering = ("is_active", "-is_superuser", "-is_staff", "-is_device", "-updated_at")
|
||||
readonly_fields = ("id", "sub", "email", "created_at", "updated_at")
|
||||
search_fields = ("id", "sub", "admin_email", "email")
|
||||
|
||||
|
||||
class ResourceAccessInline(admin.TabularInline):
|
||||
|
||||
@@ -14,8 +14,8 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["id", "email", "full_name", "short_name"]
|
||||
read_only_fields = ["id", "email", "full_name", "short_name"]
|
||||
fields = ["id", "email"]
|
||||
read_only_fields = ["id", "email"]
|
||||
|
||||
|
||||
class ResourceAccessSerializerMixin:
|
||||
|
||||
@@ -75,16 +75,11 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
email = user_info.get("email")
|
||||
user = self.get_existing_user(sub, email)
|
||||
|
||||
claims = {
|
||||
"email": email,
|
||||
"full_name": self.compute_full_name(user_info),
|
||||
"short_name": user_info.get(settings.OIDC_USERINFO_SHORTNAME_FIELD),
|
||||
}
|
||||
if not user and self.get_settings("OIDC_CREATE_USER", True):
|
||||
user = User.objects.create(
|
||||
sub=sub,
|
||||
email=email,
|
||||
password="!", # noqa: S106
|
||||
**claims,
|
||||
)
|
||||
elif not user:
|
||||
return None
|
||||
@@ -92,8 +87,6 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
if not user.is_active:
|
||||
raise SuspiciousOperation(_("User account is disabled"))
|
||||
|
||||
self.update_user_if_needed(user, claims)
|
||||
|
||||
return user
|
||||
|
||||
def get_existing_user(self, sub, email):
|
||||
@@ -111,32 +104,3 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
_("Multiple user accounts share a common email.")
|
||||
) from e
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def compute_full_name(user_info):
|
||||
"""Compute user's full name based on OIDC fields in settings."""
|
||||
full_name = " ".join(
|
||||
filter(
|
||||
None,
|
||||
(
|
||||
user_info.get(field)
|
||||
for field in settings.OIDC_USERINFO_FULLNAME_FIELDS
|
||||
),
|
||||
)
|
||||
)
|
||||
return full_name or None
|
||||
|
||||
@staticmethod
|
||||
def update_user_if_needed(user, claims):
|
||||
"""Update user claims if they have changed."""
|
||||
user_fields = vars(user.__class__) # Get available model fields
|
||||
updated_claims = {
|
||||
key: value
|
||||
for key, value in claims.items()
|
||||
if value and key in user_fields and value != getattr(user, key)
|
||||
}
|
||||
|
||||
if not updated_claims:
|
||||
return
|
||||
|
||||
User.objects.filter(sub=user.sub).update(**updated_claims)
|
||||
|
||||
@@ -23,8 +23,6 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
sub = factory.Sequence(lambda n: f"user{n!s}")
|
||||
email = factory.Faker("email")
|
||||
full_name = factory.Faker("name")
|
||||
short_name = factory.Faker("first_name")
|
||||
language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES])
|
||||
password = make_password("password")
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-13 09:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_recording_mode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='full_name',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='full name'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='short_name',
|
||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='short name'),
|
||||
)
|
||||
]
|
||||
@@ -141,14 +141,11 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
email = models.EmailField(_("identity email address"), blank=True, null=True)
|
||||
|
||||
# Unlike the "email" field which stores the email coming from the OIDC token, this field
|
||||
# stores the email used by staff users to log in to the admin site
|
||||
# stores the email used by staff users to login to the admin site
|
||||
admin_email = models.EmailField(
|
||||
_("admin email address"), unique=True, blank=True, null=True
|
||||
)
|
||||
full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True)
|
||||
short_name = models.CharField(
|
||||
_("short name"), max_length=100, null=True, blank=True
|
||||
)
|
||||
|
||||
language = models.CharField(
|
||||
max_length=10,
|
||||
choices=lazy(lambda: settings.LANGUAGES, tuple)(),
|
||||
@@ -207,6 +204,13 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
"""
|
||||
return []
|
||||
|
||||
@property
|
||||
def email_anonymized(self):
|
||||
"""Anonymize the email address by replacing the local part with asterisks."""
|
||||
if not self.email:
|
||||
return ""
|
||||
return f"***@{self.email.split('@')[1]}"
|
||||
|
||||
|
||||
def get_resource_roles(resource: models.Model, user: User) -> List[str]:
|
||||
"""
|
||||
|
||||
@@ -62,14 +62,15 @@ def test_authentication_getter_new_user_no_email(monkeypatch):
|
||||
def test_authentication_getter_new_user_with_email(monkeypatch):
|
||||
"""
|
||||
If no user matches, a user should be created.
|
||||
User's info contains an email, created user's email should be filled.
|
||||
User's email and name should be set on the identity.
|
||||
The "email" field on the User model should not be set as it is reserved for staff users.
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
email = "meet@example.com"
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123", "email": email}
|
||||
return {"sub": "123", "email": email, "first_name": "John", "last_name": "Doe"}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
@@ -79,34 +80,6 @@ def test_authentication_getter_new_user_with_email(monkeypatch):
|
||||
|
||||
assert user.sub == "123"
|
||||
assert user.email == email
|
||||
assert user.full_name is None
|
||||
assert user.short_name is None
|
||||
assert user.password == "!"
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("email", [None, "johndoe@foo.com"])
|
||||
def test_authentication_getter_new_user_with_names(monkeypatch, email):
|
||||
"""
|
||||
If no user matches, a user should be created.
|
||||
User's info contains name-related field, created user's full and short names should be filled,
|
||||
whether the email is filled
|
||||
"""
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {"sub": "123", "given_name": "John", "usual_name": "Doe", "email": email}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
assert user.sub == "123"
|
||||
assert user.email == email
|
||||
assert user.full_name == "John Doe"
|
||||
assert user.short_name == "John"
|
||||
assert user.password == "!"
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
@@ -312,103 +285,3 @@ def test_authentication_getter_existing_user_email_tricky(email, monkeypatch, se
|
||||
)
|
||||
|
||||
assert user != db_user
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"given_name, usual_name, email",
|
||||
[
|
||||
("Jack", "Doe", "john.doe@example.com"),
|
||||
("John", "Duy", "john.doe@example.com"),
|
||||
("John", "Doe", "jack.duy@example.com"),
|
||||
("Jack", "Duy", "jack.duy@example.com"),
|
||||
],
|
||||
)
|
||||
def test_authentication_getter_existing_user_change_fields(
|
||||
given_name, usual_name, email, django_assert_num_queries, monkeypatch
|
||||
):
|
||||
"""It should update the email or name fields on the user when they change."""
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
user = UserFactory(
|
||||
full_name="John Doe", short_name="John", email="john.doe@example.com"
|
||||
)
|
||||
|
||||
def get_userinfo_mocked(*args):
|
||||
return {
|
||||
"sub": user.sub,
|
||||
"email": email,
|
||||
"given_name": given_name,
|
||||
"usual_name": usual_name,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# One and only one additional update query when a field has changed
|
||||
with django_assert_num_queries(2):
|
||||
authenticated_user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
assert user == authenticated_user
|
||||
user.refresh_from_db()
|
||||
assert user.email == email
|
||||
assert user.full_name == f"{given_name:s} {usual_name:s}"
|
||||
assert user.short_name == given_name
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_info, expected_name",
|
||||
[
|
||||
({"given_name": "John", "family_name": "Doe"}, "John Doe"),
|
||||
(
|
||||
{"given_name": "John", "middle_name": "M", "family_name": "Doe"},
|
||||
"John M Doe",
|
||||
),
|
||||
({"family_name": "Doe"}, "Doe"),
|
||||
({"given_name": "", "family_name": ""}, None),
|
||||
({}, None),
|
||||
],
|
||||
)
|
||||
def test_compute_full_name(user_info, expected_name, settings):
|
||||
"""Test full name computation from OIDC user info fields."""
|
||||
settings.OIDC_USERINFO_FULLNAME_FIELDS = [
|
||||
"given_name",
|
||||
"middle_name",
|
||||
"family_name",
|
||||
]
|
||||
klass = OIDCAuthenticationBackend()
|
||||
assert klass.compute_full_name(user_info) == expected_name
|
||||
|
||||
|
||||
def test_compute_full_name_no_fields(settings):
|
||||
"""Test full name computation with empty field configuration."""
|
||||
settings.OIDC_USERINFO_FULLNAME_FIELDS = []
|
||||
klass = OIDCAuthenticationBackend()
|
||||
assert klass.compute_full_name({"given_name": "John"}) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"claims",
|
||||
[
|
||||
{"email": "john.doe@example.com"}, # Same data - no change needed
|
||||
{"email": ""}, # Empty strings should not override
|
||||
{"non_related_field": "foo"}, # Unrelated fields should be ignored
|
||||
{}, # Empty claims should not affect user
|
||||
{"email": None}, # None values should be ignored
|
||||
],
|
||||
)
|
||||
def test_update_user_when_no_update_needed(django_assert_num_queries, claims):
|
||||
"""Test that user attributes remain unchanged when claims don't require updates."""
|
||||
|
||||
user = UserFactory(
|
||||
full_name="John Doe", short_name="John", email="john.doe@example.com"
|
||||
)
|
||||
|
||||
klass = OIDCAuthenticationBackend()
|
||||
|
||||
with django_assert_num_queries(0):
|
||||
klass.update_user_if_needed(user, claims)
|
||||
|
||||
user.refresh_from_db()
|
||||
|
||||
assert user.email == "john.doe@example.com"
|
||||
|
||||
@@ -301,8 +301,6 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries):
|
||||
"user": {
|
||||
"id": str(user_access.user.id),
|
||||
"email": user_access.user.email,
|
||||
"full_name": user_access.user.full_name,
|
||||
"short_name": user_access.user.short_name,
|
||||
},
|
||||
"resource": str(room.id),
|
||||
"role": user_access.role,
|
||||
@@ -312,8 +310,6 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries):
|
||||
"user": {
|
||||
"id": str(other_user_access.user.id),
|
||||
"email": other_user_access.user.email,
|
||||
"full_name": other_user_access.user.full_name,
|
||||
"short_name": other_user_access.user.short_name,
|
||||
},
|
||||
"resource": str(room.id),
|
||||
"role": other_user_access.role,
|
||||
@@ -378,8 +374,6 @@ def test_api_rooms_retrieve_administrators(mock_token, django_assert_num_queries
|
||||
"user": {
|
||||
"id": str(other_user_access.user.id),
|
||||
"email": other_user_access.user.email,
|
||||
"full_name": other_user_access.user.full_name,
|
||||
"short_name": other_user_access.user.short_name,
|
||||
},
|
||||
"resource": str(room.id),
|
||||
"role": other_user_access.role,
|
||||
@@ -389,8 +383,6 @@ def test_api_rooms_retrieve_administrators(mock_token, django_assert_num_queries
|
||||
"user": {
|
||||
"id": str(user_access.user.id),
|
||||
"email": user_access.user.email,
|
||||
"full_name": user_access.user.full_name,
|
||||
"short_name": user_access.user.short_name,
|
||||
},
|
||||
"resource": str(room.id),
|
||||
"role": user_access.role,
|
||||
|
||||
@@ -96,8 +96,6 @@ def test_api_users_retrieve_me_authenticated():
|
||||
assert response.json() == {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"short_name": user.short_name,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -44,3 +44,12 @@ def test_models_users_send_mail_main_missing():
|
||||
user.email_user("my subject", "my message")
|
||||
|
||||
assert str(excinfo.value) == "User has no email address."
|
||||
|
||||
|
||||
def test_models_users_email_anonymized():
|
||||
"""The user's email should be anonymized if it exists."""
|
||||
user = factories.UserFactory(email="john.doe@world.com")
|
||||
assert user.email_anonymized == "***@world.com"
|
||||
|
||||
user = factories.UserFactory(email=None)
|
||||
assert user.email_anonymized == ""
|
||||
|
||||
@@ -390,16 +390,6 @@ class Base(Configuration):
|
||||
OIDC_REDIRECT_FIELD_NAME = values.Value(
|
||||
"returnTo", environ_name="OIDC_REDIRECT_FIELD_NAME", environ_prefix=None
|
||||
)
|
||||
OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue(
|
||||
default=["given_name", "usual_name"],
|
||||
environ_name="OIDC_USERINFO_FULLNAME_FIELDS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_USERINFO_SHORTNAME_FIELD = values.Value(
|
||||
default="given_name",
|
||||
environ_name="OIDC_USERINFO_SHORTNAME_FIELD",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Video conference configuration
|
||||
LIVEKIT_CONFIGURATION = {
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "0.1.8"
|
||||
version = "0.1.7"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
Generated
+7
-21
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.7",
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.6.5",
|
||||
"@livekit/components-styles": "1.1.3",
|
||||
@@ -22,7 +22,7 @@
|
||||
"i18next-parser": "9.0.2",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.5.7",
|
||||
"posthog-js": "1.186.1",
|
||||
"posthog-js": "1.167.0",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.4.0",
|
||||
"react-dom": "18.3.1",
|
||||
@@ -409,7 +409,6 @@
|
||||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"extraneous": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4530,17 +4529,6 @@
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.39.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
|
||||
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
@@ -8265,15 +8253,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.186.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.186.1.tgz",
|
||||
"integrity": "sha512-m6TNW01nfqErwMxaZxNScYdMaUJO0s3bbmt/tboL29yZDnuHdOiYFbG+T4MCxdFxjWRa5gOR25bQD/SSt1t/4A==",
|
||||
"license": "MIT",
|
||||
"version": "1.167.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.167.0.tgz",
|
||||
"integrity": "sha512-/zXQ6tuJgiF1d4mgg3UsAi/uoyg7UnfFNQtikuALmaE53xFExpcAKbMfHPG/f54QgTvLxSHyGL1kFl/1uspkGg==",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.0"
|
||||
"web-vitals": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -27,7 +27,7 @@
|
||||
"i18next-parser": "9.0.2",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.5.7",
|
||||
"posthog-js": "1.186.1",
|
||||
"posthog-js": "1.167.0",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.4.0",
|
||||
"react-dom": "18.3.1",
|
||||
|
||||
@@ -40,7 +40,6 @@ const config: Config = {
|
||||
// media queries are defined in em so that zooming with text-only mode triggers breakpoints
|
||||
breakpoints: {
|
||||
xs: '22.6em', // 360px (we assume less than that are old/entry level mobile phones)
|
||||
xsm: '31.25em', // 500px,
|
||||
sm: '40em', // 640px
|
||||
md: '48em', // 768px
|
||||
lg: '64em', // 1024px
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 635 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 702 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 599 KiB |
@@ -0,0 +1,25 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { RiExternalLinkLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LinkButton } from '@/primitives'
|
||||
|
||||
export const Feedback = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<LinkButton
|
||||
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
|
||||
variant="success"
|
||||
target="_blank"
|
||||
>
|
||||
<span className={css({ marginRight: 0.5 })} aria-hidden="true">
|
||||
💡
|
||||
</span>
|
||||
{t('feedbackAlert')}
|
||||
<RiExternalLinkLine
|
||||
size={16}
|
||||
className={css({ marginLeft: 0.5 })}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</LinkButton>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { RiErrorWarningLine, RiExternalLinkLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Text, A } from '@/primitives'
|
||||
|
||||
const GRIST_FORM =
|
||||
'https://grist.numerique.gouv.fr/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4'
|
||||
|
||||
export const FeedbackBanner = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
backgroundColor: '#E8EDFF',
|
||||
color: '#0063CB',
|
||||
display: { base: 'none', sm: 'flex' },
|
||||
justifyContent: 'center',
|
||||
padding: '0.5rem 0',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'inline-flex',
|
||||
gap: '0.5rem',
|
||||
alignItems: 'center',
|
||||
})}
|
||||
>
|
||||
<RiErrorWarningLine size={20} />
|
||||
<Text as="p">{t('feedback.context')}</Text>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.25,
|
||||
})}
|
||||
>
|
||||
<A href={GRIST_FORM} target="_blank">
|
||||
{t('feedback.cta')}
|
||||
</A>
|
||||
<RiExternalLinkLine size={16} aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
export type ApiUser = {
|
||||
id: string
|
||||
email: string
|
||||
full_name: string
|
||||
last_name: string
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
import firstSlide from '@/assets/intro-slider/1_solo.png'
|
||||
import secondSlide from '@/assets/intro-slider/2_multiple.png'
|
||||
import thirdSlide from '@/assets/intro-slider/3_resume.png'
|
||||
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Button, LinkButton } from '@/primitives'
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// todo - extract in a proper env variable
|
||||
const BETA_USERS_FORM_URL =
|
||||
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17'
|
||||
|
||||
const Heading = styled('h2', {
|
||||
base: {
|
||||
width: 'fit-content',
|
||||
marginBottom: 0,
|
||||
fontSize: '1.5rem',
|
||||
marginTop: '0.75rem',
|
||||
lineHeight: '2rem',
|
||||
maxWidth: '23rem',
|
||||
textAlign: 'center',
|
||||
textWrap: 'pretty',
|
||||
},
|
||||
})
|
||||
|
||||
const Body = styled('p', {
|
||||
base: {
|
||||
maxWidth: '23rem',
|
||||
textAlign: 'center',
|
||||
textWrap: 'pretty',
|
||||
lineHeight: '1.2rem',
|
||||
fontSize: '1rem',
|
||||
},
|
||||
})
|
||||
|
||||
const Image = styled('img', {
|
||||
base: {
|
||||
maxHeight: '362px',
|
||||
height: '100%',
|
||||
width: 'fit-content',
|
||||
},
|
||||
})
|
||||
|
||||
const Dot = styled('div', {
|
||||
base: {
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block',
|
||||
height: '.375rem',
|
||||
margin: '0 .25rem',
|
||||
width: '.375rem',
|
||||
},
|
||||
variants: {
|
||||
selected: {
|
||||
true: {
|
||||
backgroundColor: '#000091',
|
||||
},
|
||||
false: {
|
||||
backgroundColor: '#CACAFB',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Container = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
const ButtonContainer = styled('div', {
|
||||
base: {
|
||||
display: { base: 'none', xsm: 'block' },
|
||||
},
|
||||
})
|
||||
|
||||
const ButtonVerticalCenter = styled('div', {
|
||||
base: {
|
||||
marginTop: '13.3125rem',
|
||||
transform: 'translateY(-50%)',
|
||||
},
|
||||
})
|
||||
|
||||
const SlideContainer = styled('div', {
|
||||
base: {
|
||||
alignItems: 'stretch',
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
},
|
||||
})
|
||||
|
||||
const Slide = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
justifyContent: 'start',
|
||||
minHeight: { base: 'none', xsm: '550px' },
|
||||
minWidth: { base: 'none', xsm: '200px' },
|
||||
width: { base: '100%', xsm: '22.625rem' },
|
||||
},
|
||||
variants: {
|
||||
visible: {
|
||||
true: {
|
||||
visibility: 'visible',
|
||||
position: 'static',
|
||||
},
|
||||
false: {
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
visible: false,
|
||||
},
|
||||
})
|
||||
|
||||
const TextAnimation = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
},
|
||||
variants: {
|
||||
visible: {
|
||||
true: {
|
||||
opacity: 1,
|
||||
transform: 'none',
|
||||
transition: 'opacity ease-in .3s, transform ease-in .3s',
|
||||
},
|
||||
false: {
|
||||
opacity: 0,
|
||||
transform: 'translateX(-30%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type Slide = {
|
||||
key: string
|
||||
img: string
|
||||
isAvailableInBeta?: boolean
|
||||
}
|
||||
|
||||
// todo - optimize how images are imported
|
||||
const SLIDES: Slide[] = [
|
||||
{
|
||||
key: 'slide1',
|
||||
img: firstSlide,
|
||||
},
|
||||
{
|
||||
key: 'slide2',
|
||||
img: secondSlide,
|
||||
},
|
||||
{
|
||||
key: 'slide3',
|
||||
img: thirdSlide,
|
||||
isAvailableInBeta: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const IntroSlider = () => {
|
||||
const [slideIndex, setSlideIndex] = useState(0)
|
||||
const { t } = useTranslation('home', { keyPrefix: 'introSlider' })
|
||||
const NUMBER_SLIDES = SLIDES.length
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
})}
|
||||
>
|
||||
<ButtonContainer>
|
||||
<ButtonVerticalCenter>
|
||||
<Button
|
||||
square
|
||||
invisible
|
||||
aria-label={t('previous.label')}
|
||||
tooltip={t('previous.tooltip')}
|
||||
onPress={() => setSlideIndex(slideIndex - 1)}
|
||||
isDisabled={slideIndex == 0}
|
||||
>
|
||||
<RiArrowLeftSLine />
|
||||
</Button>
|
||||
</ButtonVerticalCenter>
|
||||
</ButtonContainer>
|
||||
<SlideContainer>
|
||||
{SLIDES.map((slide, index) => (
|
||||
<Slide visible={index == slideIndex} key={index}>
|
||||
<Image src={slide.img} alt={t(`${slide.key}.imgAlt`)} />
|
||||
<TextAnimation visible={index == slideIndex}>
|
||||
<Heading>{t(`${slide.key}.title`)}</Heading>
|
||||
<Body>{t(`${slide.key}.body`)}</Body>
|
||||
{slide.isAvailableInBeta && (
|
||||
<LinkButton
|
||||
href={BETA_USERS_FORM_URL}
|
||||
tooltip={t('beta.tooltip')}
|
||||
variant={'primary'}
|
||||
size={'sm'}
|
||||
style={{ marginTop: '1rem', width: 'fit-content' }}
|
||||
>
|
||||
{t('beta.text')}
|
||||
</LinkButton>
|
||||
)}
|
||||
</TextAnimation>
|
||||
</Slide>
|
||||
))}
|
||||
</SlideContainer>
|
||||
<ButtonContainer>
|
||||
<ButtonVerticalCenter>
|
||||
<Button
|
||||
square
|
||||
invisible
|
||||
aria-label={t('next.label')}
|
||||
tooltip={t('next.tooltip')}
|
||||
onPress={() => setSlideIndex(slideIndex + 1)}
|
||||
isDisabled={slideIndex == NUMBER_SLIDES - 1}
|
||||
>
|
||||
<RiArrowRightSLine />
|
||||
</Button>
|
||||
</ButtonVerticalCenter>
|
||||
</ButtonContainer>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
marginTop: '0.5rem',
|
||||
display: { base: 'none', xsm: 'block' },
|
||||
})}
|
||||
>
|
||||
{SLIDES.map((_, index) => (
|
||||
<Dot key={index} selected={index == slideIndex} />
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { A, Text } from '@/primitives'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const MANIFEST_LINK =
|
||||
'https://docs.numerique.gouv.fr/docs/1ef86abf-f7e0-46ce-b6c7-8be8b8af4c3d/'
|
||||
|
||||
export const MoreLink = () => {
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
return (
|
||||
<Text as={'p'} variant={'sm'} style={{ padding: '1rem 0' }}>
|
||||
<A
|
||||
href={MANIFEST_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t('moreLinkLabel')}
|
||||
>
|
||||
{t('moreLink')}
|
||||
</A>{' '}
|
||||
{t('moreAbout')}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DialogTrigger, MenuItem, Menu as RACMenu } from 'react-aria-components'
|
||||
import { Button, Menu } from '@/primitives'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
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 { useUser, UserAware } from '@/features/auth'
|
||||
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
||||
@@ -13,132 +14,7 @@ import { usePersistentUserChoices } from '@livekit/components-react'
|
||||
import { menuItemRecipe } from '@/primitives/menuItemRecipe'
|
||||
import { RiAddLine, RiLink } from '@remixicon/react'
|
||||
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||
import { IntroSlider } from '@/features/home/components/IntroSlider'
|
||||
import { MoreLink } from '@/features/home/components/MoreLink'
|
||||
import { ReactNode, useState } from 'react'
|
||||
|
||||
import { css } from '@/styled-system/css'
|
||||
|
||||
const Columns = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
alignItems: 'center',
|
||||
margin: 'auto',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
minHeight: '100%',
|
||||
justifyContent: 'normal',
|
||||
padding: '0 1rem',
|
||||
width: 'calc(100% - 2rem)',
|
||||
opacity: 0,
|
||||
animation: '.5s ease-in fade 0s forwards',
|
||||
lg: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const LeftColumn = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
flexBasis: 'auto',
|
||||
flexShrink: 0,
|
||||
maxWidth: '38rem',
|
||||
width: '100%',
|
||||
padding: '1rem 3%',
|
||||
marginTop: 'auto',
|
||||
lg: {
|
||||
margin: 0,
|
||||
textAlign: 'left',
|
||||
alignItems: 'flex-start',
|
||||
flexShrink: '1',
|
||||
flexBasis: '36rem',
|
||||
maxWidth: '36rem',
|
||||
padding: '1em 3em',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RightColumn = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
padding: '1rem 3%',
|
||||
marginBottom: 'auto',
|
||||
flexBasis: 'auto',
|
||||
flexShrink: 0,
|
||||
maxWidth: '39rem',
|
||||
lg: {
|
||||
margin: 0,
|
||||
flexBasis: '45%',
|
||||
padding: '1em 3em',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Separator = styled('div', {
|
||||
base: {
|
||||
borderBottom: '1px solid',
|
||||
borderColor: '#747775',
|
||||
marginTop: '2.5rem',
|
||||
maxWidth: '30rem',
|
||||
width: '100%',
|
||||
},
|
||||
})
|
||||
|
||||
const Heading = styled('h1', {
|
||||
base: {
|
||||
fontWeight: '500',
|
||||
fontStyle: 'normal',
|
||||
fontStretch: 'normal',
|
||||
fontOpticalSizing: 'auto',
|
||||
marginBottom: 0,
|
||||
paddingBottom: '0.75rem',
|
||||
fontSize: '2.3rem',
|
||||
lineHeight: '2.5rem',
|
||||
letterSpacing: '0',
|
||||
xsm: {
|
||||
fontSize: '3rem',
|
||||
lineHeight: '3.2rem',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const IntroText = styled('div', {
|
||||
base: {
|
||||
marginBottom: '3rem',
|
||||
fontSize: '1.5rem',
|
||||
lineHeight: '1.8rem',
|
||||
textWrap: 'pretty',
|
||||
maxWidth: '32rem',
|
||||
},
|
||||
})
|
||||
import { useState } from 'react'
|
||||
|
||||
export const Home = () => {
|
||||
const { t } = useTranslation('home')
|
||||
@@ -154,90 +30,72 @@ export const Home = () => {
|
||||
return (
|
||||
<UserAware>
|
||||
<Screen>
|
||||
<Columns>
|
||||
<LeftColumn>
|
||||
<Heading>{t('heading')}</Heading>
|
||||
<IntroText>{t('intro')}</IntroText>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: 0.5,
|
||||
flexDirection: { base: 'column', xsm: 'row' },
|
||||
alignItems: { base: 'center', xsm: 'items-start' },
|
||||
})}
|
||||
>
|
||||
{isLoggedIn ? (
|
||||
<Menu>
|
||||
<Button variant="primary" data-attr="create-meeting">
|
||||
{t('createMeeting')}
|
||||
</Button>
|
||||
<RACMenu>
|
||||
<MenuItem
|
||||
className={menuItemRecipe({ icon: true })}
|
||||
onAction={async () => {
|
||||
const slug = generateRoomId()
|
||||
createRoom({ slug, username }).then((data) =>
|
||||
navigateTo('room', data.slug, {
|
||||
state: { create: true, initialRoomData: data },
|
||||
})
|
||||
)
|
||||
}}
|
||||
data-attr="create-option-instant"
|
||||
>
|
||||
<RiAddLine size={18} />
|
||||
{t('createMenu.instantOption')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={menuItemRecipe({ icon: true })}
|
||||
onAction={() => {
|
||||
const slug = generateRoomId()
|
||||
createRoom({ slug, username }).then((data) =>
|
||||
setLaterRoomId(data.slug)
|
||||
)
|
||||
}}
|
||||
data-attr="create-option-later"
|
||||
>
|
||||
<RiLink size={18} />
|
||||
{t('createMenu.laterOption')}
|
||||
</MenuItem>
|
||||
</RACMenu>
|
||||
</Menu>
|
||||
) : (
|
||||
<ProConnectButton hint={false} />
|
||||
)}
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
variant="primary"
|
||||
outline
|
||||
style={{
|
||||
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
|
||||
}}
|
||||
>
|
||||
{t('joinMeeting')}
|
||||
<Centered width="fit-content">
|
||||
<Text as="h1" variant="display">
|
||||
{t('heading')}
|
||||
</Text>
|
||||
<Text as="p" variant="h3">
|
||||
{t('intro')}
|
||||
</Text>
|
||||
{!isLoggedIn && (
|
||||
<Text margin="sm" variant="note">
|
||||
{t('loginToCreateMeeting')}
|
||||
</Text>
|
||||
)}
|
||||
<HStack gap="gutter" alignItems="start">
|
||||
{isLoggedIn ? (
|
||||
<Menu>
|
||||
<Button variant="primary" data-attr="create-meeting">
|
||||
{t('createMeeting')}
|
||||
</Button>
|
||||
<JoinMeetingDialog />
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
<Separator />
|
||||
<div
|
||||
className={css({
|
||||
display: { base: 'none', lg: 'inline' },
|
||||
})}
|
||||
>
|
||||
<MoreLink />
|
||||
</div>
|
||||
</LeftColumn>
|
||||
<RightColumn>
|
||||
<IntroSlider />
|
||||
<div
|
||||
className={css({
|
||||
display: { base: 'inline', lg: 'none' },
|
||||
})}
|
||||
>
|
||||
<MoreLink />
|
||||
</div>
|
||||
</RightColumn>
|
||||
</Columns>
|
||||
<RACMenu>
|
||||
<MenuItem
|
||||
className={menuItemRecipe({ icon: true })}
|
||||
onAction={async () => {
|
||||
const slug = generateRoomId()
|
||||
createRoom({ slug, username }).then((data) =>
|
||||
navigateTo('room', data.slug, {
|
||||
state: { create: true, initialRoomData: data },
|
||||
})
|
||||
)
|
||||
}}
|
||||
data-attr="create-option-instant"
|
||||
>
|
||||
<RiAddLine size={18} />
|
||||
{t('createMenu.instantOption')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={menuItemRecipe({ icon: true })}
|
||||
onAction={() => {
|
||||
const slug = generateRoomId()
|
||||
createRoom({ slug, username }).then((data) =>
|
||||
setLaterRoomId(data.slug)
|
||||
)
|
||||
}}
|
||||
data-attr="create-option-later"
|
||||
>
|
||||
<RiLink size={18} />
|
||||
{t('createMenu.laterOption')}
|
||||
</MenuItem>
|
||||
</RACMenu>
|
||||
</Menu>
|
||||
) : (
|
||||
<ProConnectButton />
|
||||
)}
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
variant="primary"
|
||||
outline
|
||||
style={{
|
||||
height: !isLoggedIn ? '56px' : undefined, // Temporary, Align with ProConnect Button fixed height
|
||||
}}
|
||||
>
|
||||
{t('joinMeeting')}
|
||||
</Button>
|
||||
<JoinMeetingDialog />
|
||||
</DialogTrigger>
|
||||
</HStack>
|
||||
</Centered>
|
||||
<LaterMeetingDialog
|
||||
roomId={laterRoomId || ''}
|
||||
onOpenChange={() => setLaterRoomId(null)}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const Conference = ({
|
||||
// These settings help ensure successful connections in poor network conditions
|
||||
const connectOptions = {
|
||||
maxRetries: 5, // Default: 1. Only for unreachable server scenarios
|
||||
peerConnectionTimeout: 60000, // Default: 15s. Extended for slow TURN/TLS negotiation
|
||||
peerConnectionTimeout: 60, // Default: 15s. Extended for slow TURN/TLS negotiation
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import { PreJoin, type LocalUserChoices } from '@livekit/components-react'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
import { useUser } from '@/features/auth'
|
||||
|
||||
export const Join = ({
|
||||
onSubmit,
|
||||
@@ -10,7 +9,6 @@ export const Join = ({
|
||||
onSubmit: (choices: LocalUserChoices) => void
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms')
|
||||
const { user } = useUser()
|
||||
|
||||
return (
|
||||
<Screen layout="centered">
|
||||
@@ -22,7 +20,6 @@ export const Join = ({
|
||||
camLabel={t('join.videoinput.label')}
|
||||
joinLabel={t('join.joinLabel')}
|
||||
userLabel={t('join.usernameLabel')}
|
||||
defaults={{ username: user?.full_name }}
|
||||
/>
|
||||
</CenteredContent>
|
||||
</Screen>
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
import { Button, H, Text, TextArea } from '@/primitives'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { cva } from '@/styled-system/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { styled, VStack } from '@/styled-system/jsx'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { PostHog } from 'posthog-js'
|
||||
|
||||
const Card = styled('div', {
|
||||
base: {
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
padding: '1rem',
|
||||
marginTop: '1.5rem',
|
||||
borderRadius: '0.25rem',
|
||||
boxShadow: '',
|
||||
minWidth: '380px',
|
||||
minHeight: '196px',
|
||||
},
|
||||
})
|
||||
|
||||
const Bar = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.300',
|
||||
borderRadius: '8px',
|
||||
overflowY: 'hidden',
|
||||
scrollbar: 'hidden',
|
||||
},
|
||||
})
|
||||
|
||||
const ratingButtonRecipe = cva({
|
||||
base: {
|
||||
backgroundColor: 'white',
|
||||
color: 'initial',
|
||||
border: 'none',
|
||||
borderRadius: 0,
|
||||
padding: '0.5rem 0.85rem',
|
||||
flexGrow: '1',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
variants: {
|
||||
selected: {
|
||||
true: {
|
||||
backgroundColor: '#1d4ed8',
|
||||
color: 'white',
|
||||
},
|
||||
false: {
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'gray.100',
|
||||
},
|
||||
},
|
||||
},
|
||||
borderLeft: {
|
||||
true: {
|
||||
borderLeft: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const labelRecipe = cva({
|
||||
base: {
|
||||
color: 'gray.600',
|
||||
paddingTop: '0.25rem',
|
||||
},
|
||||
})
|
||||
|
||||
const OpenFeedback = ({
|
||||
posthog,
|
||||
onNext,
|
||||
}: {
|
||||
posthog: PostHog
|
||||
onNext: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'openFeedback' })
|
||||
const [feedback, setFeedback] = useState('')
|
||||
|
||||
const onContinue = () => {
|
||||
setFeedback('')
|
||||
onNext()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
try {
|
||||
posthog.capture('survey sent', {
|
||||
$survey_id: '01933c5a-5a1d-0000-ada8-e39f5918c2d4',
|
||||
$survey_response: feedback,
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
} finally {
|
||||
onContinue()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<H lvl={3}>{t('question')}</H>
|
||||
<TextArea
|
||||
id="feedbackInput"
|
||||
name="feedback"
|
||||
placeholder={t('placeholder')}
|
||||
required
|
||||
value={feedback}
|
||||
onChange={(e) => setFeedback(e.target.value)}
|
||||
style={{
|
||||
minHeight: '150px',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
/>
|
||||
<VStack gap="0.5">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
fullWidth
|
||||
isDisabled={!feedback}
|
||||
onPress={onSubmit}
|
||||
>
|
||||
{t('submit')}
|
||||
</Button>
|
||||
<Button invisible size="sm" fullWidth onPress={onNext}>
|
||||
{t('skip')}
|
||||
</Button>
|
||||
</VStack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const RateQuality = ({
|
||||
posthog,
|
||||
onNext,
|
||||
maxRating = 7,
|
||||
}: {
|
||||
posthog: PostHog
|
||||
onNext: () => void
|
||||
maxRating?: number
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'rating' })
|
||||
const [selectedRating, setSelectedRating] = useState<number | null>(null)
|
||||
|
||||
const handleRatingClick = (rating: number) => {
|
||||
setSelectedRating((prevRating) => (prevRating === rating ? null : rating))
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
try {
|
||||
posthog.capture('survey sent', {
|
||||
$survey_id: '01933c22-d005-0000-b623-20b752171e2e',
|
||||
$survey_response: `${selectedRating}`,
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
} finally {
|
||||
setSelectedRating(null)
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<H lvl={3}>{t('question')}</H>
|
||||
<Bar>
|
||||
{[...Array(maxRating)].map((_, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onPress={() => handleRatingClick(index + 1)}
|
||||
className={ratingButtonRecipe({
|
||||
selected: selectedRating === index + 1,
|
||||
borderLeft: index != 0,
|
||||
})}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</Bar>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
<Text variant="sm" className={labelRecipe()}>
|
||||
{t('levels.min')}
|
||||
</Text>
|
||||
<Text variant="sm" className={labelRecipe()}>
|
||||
{t('levels.max')}
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
fullWidth
|
||||
isDisabled={!selectedRating}
|
||||
onPress={onSubmit}
|
||||
>
|
||||
{t('submit')}
|
||||
</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const ConfirmationMessage = ({ onNext }: { onNext: () => void }) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'confirmationMessage' })
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onNext()
|
||||
}, 10000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [onNext])
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
maxWidth: '380px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<VStack gap={0}>
|
||||
<H lvl={3}>{t('heading')}</H>
|
||||
<Text as="p" variant="paragraph" centered>
|
||||
{t('body')}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export const Rating = () => {
|
||||
const posthog = usePostHog()
|
||||
|
||||
const [step, setStep] = useState(0)
|
||||
|
||||
if (step == 0) {
|
||||
return <RateQuality posthog={posthog} onNext={() => setStep(step + 1)} />
|
||||
}
|
||||
|
||||
if (step == 1) {
|
||||
return <OpenFeedback posthog={posthog} onNext={() => setStep(step + 1)} />
|
||||
}
|
||||
|
||||
if (step == 2) {
|
||||
return <ConfirmationMessage onNext={() => setStep(0)} />
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Button, TextArea } from '@/primitives'
|
||||
import { Button } from '@/primitives'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { RiSendPlane2Fill } from '@remixicon/react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { TextArea } from '@/primitives/TextArea'
|
||||
import { RefObject } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
|
||||
@@ -149,13 +149,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
const { isSidePanelOpen } = useSidePanel()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="lk-video-conference"
|
||||
{...props}
|
||||
style={{
|
||||
overflowX: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div className="lk-video-conference" {...props}>
|
||||
{isWeb() && (
|
||||
<LayoutContextProvider
|
||||
value={layoutContext}
|
||||
|
||||
@@ -1,47 +1,17 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/primitives'
|
||||
import { Text } from '@/primitives'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { Center, HStack, styled, VStack } from '@/styled-system/jsx'
|
||||
import { Rating } from '@/features/rooms/components/Rating.tsx'
|
||||
import { useLocation } from 'wouter'
|
||||
|
||||
// fixme - duplicated with home, refactor in a proper style
|
||||
const Heading = styled('h1', {
|
||||
base: {
|
||||
fontWeight: '500',
|
||||
fontStyle: 'normal',
|
||||
fontStretch: 'normal',
|
||||
fontOpticalSizing: 'auto',
|
||||
fontSize: '2.3rem',
|
||||
lineHeight: '2.5rem',
|
||||
letterSpacing: '0',
|
||||
paddingBottom: '2rem',
|
||||
},
|
||||
})
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
|
||||
export const FeedbackRoute = () => {
|
||||
const { t } = useTranslation('rooms')
|
||||
const [, setLocation] = useLocation()
|
||||
return (
|
||||
<Screen layout="centered">
|
||||
<Center>
|
||||
<VStack>
|
||||
<Heading>{t('feedback.heading')}</Heading>
|
||||
<HStack>
|
||||
<Button
|
||||
outline
|
||||
variant="primary"
|
||||
onPress={() => window.history.back()}
|
||||
>
|
||||
{t('feedback.back')}
|
||||
</Button>
|
||||
<Button variant="primary" onPress={() => setLocation('/')}>
|
||||
{t('feedback.home')}
|
||||
</Button>
|
||||
</HStack>
|
||||
<Rating />
|
||||
</VStack>
|
||||
</Center>
|
||||
<CenteredContent title={t('feedback.heading')} withBackButton>
|
||||
<Text as="p" variant="h3" centered>
|
||||
{t('feedback.body')}
|
||||
</Text>
|
||||
</CenteredContent>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SettingsDialog = (props: SettingsDialogProps) => {
|
||||
<P>
|
||||
<Trans
|
||||
i18nKey="settings:account.currentlyLoggedAs"
|
||||
values={{ user: user?.full_name || user?.email }}
|
||||
values={{ user: user?.email }}
|
||||
components={[<Badge />]}
|
||||
/>
|
||||
</P>
|
||||
|
||||
@@ -48,7 +48,7 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
|
||||
<P>
|
||||
<Trans
|
||||
i18nKey="settings:account.currentlyLoggedAs"
|
||||
values={{ user: user?.full_name || user?.email }}
|
||||
values={{ user: user?.email }}
|
||||
components={[<Badge />]}
|
||||
/>
|
||||
</P>
|
||||
|
||||
@@ -13,14 +13,11 @@ i18n
|
||||
)
|
||||
.use(initReactI18next)
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
supportedLngs: ['en', 'fr'],
|
||||
fallbackLng: 'fr',
|
||||
ns: i18nDefaultNamespace,
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
},
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
})
|
||||
i18n.init({
|
||||
supportedLngs: ['en', 'fr'],
|
||||
fallbackLng: 'en',
|
||||
ns: i18nDefaultNamespace,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Text, Button } from '@/primitives'
|
||||
import { SettingsButton } from '@/features/settings'
|
||||
import { logoutUrl, useUser } from '@/features/auth'
|
||||
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
|
||||
import { FeedbackBanner } from '@/components/FeedbackBanner.tsx'
|
||||
import { Feedback } from '@/components/Feedback'
|
||||
import { Menu } from '@/primitives/Menu'
|
||||
import { MenuList } from '@/primitives/MenuList'
|
||||
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||
@@ -20,85 +20,80 @@ export const Header = () => {
|
||||
const { user, isLoggedIn } = useUser()
|
||||
|
||||
return (
|
||||
<>
|
||||
<FeedbackBanner />
|
||||
<div
|
||||
className={css({
|
||||
borderBottomColor: 'box.border',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomStyle: 'solid',
|
||||
paddingY: 1,
|
||||
paddingX: 1,
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
paddingY: 1,
|
||||
paddingX: 1,
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
rowGap: 1,
|
||||
md: {
|
||||
rowGap: 0,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
})}
|
||||
>
|
||||
<header>
|
||||
<Stack gap={2.25} direction="row" align="center">
|
||||
<Text bold variant="h1" margin={false}>
|
||||
<Link
|
||||
onClick={(event) => {
|
||||
if (
|
||||
isRoom &&
|
||||
!window.confirm(t('leaveRoomPrompt', { ns: 'rooms' }))
|
||||
) {
|
||||
event.preventDefault()
|
||||
},
|
||||
})}
|
||||
>
|
||||
<header>
|
||||
<Stack gap={2.25} direction="row" align="center">
|
||||
<Text bold variant="h1" margin={false}>
|
||||
<Link
|
||||
onClick={(event) => {
|
||||
if (
|
||||
isRoom &&
|
||||
!window.confirm(t('leaveRoomPrompt', { ns: 'rooms' }))
|
||||
) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}}
|
||||
to="/"
|
||||
>
|
||||
{t('app')}
|
||||
</Link>
|
||||
</Text>
|
||||
<Feedback />
|
||||
</Stack>
|
||||
</header>
|
||||
<nav>
|
||||
<Stack gap={1} direction="row" align="center">
|
||||
{isLoggedIn === false && !isHome && (
|
||||
<ProConnectButton hint={false} />
|
||||
)}
|
||||
{!!user && (
|
||||
<Menu>
|
||||
<Button
|
||||
size="sm"
|
||||
invisible
|
||||
tooltip={t('loggedInUserTooltip')}
|
||||
tooltipType="delayed"
|
||||
>
|
||||
{user.email}
|
||||
</Button>
|
||||
<MenuList
|
||||
items={[{ value: 'logout', label: t('logout') }]}
|
||||
onAction={(value) => {
|
||||
if (value === 'logout') {
|
||||
terminateAnalyticsSession()
|
||||
terminateSupportSession()
|
||||
window.location.href = logoutUrl()
|
||||
}
|
||||
}}
|
||||
to="/"
|
||||
>
|
||||
{t('app')}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
</header>
|
||||
<nav>
|
||||
<Stack gap={1} direction="row" align="center">
|
||||
{isLoggedIn === false && !isHome && (
|
||||
<ProConnectButton hint={false} />
|
||||
)}
|
||||
{!!user && (
|
||||
<Menu>
|
||||
<Button
|
||||
size="sm"
|
||||
invisible
|
||||
tooltip={t('loggedInUserTooltip')}
|
||||
tooltipType="delayed"
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'block',
|
||||
maxWidth: { base: '80px', xsm: '350px' },
|
||||
})}
|
||||
>
|
||||
{user?.full_name || user?.email}
|
||||
</span>
|
||||
</Button>
|
||||
<MenuList
|
||||
items={[{ value: 'logout', label: t('logout') }]}
|
||||
onAction={(value) => {
|
||||
if (value === 'logout') {
|
||||
terminateAnalyticsSession()
|
||||
terminateSupportSession()
|
||||
window.location.href = logoutUrl()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
<SettingsButton />
|
||||
</Stack>
|
||||
</nav>
|
||||
</div>
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
<SettingsButton />
|
||||
</Stack>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
"error": {
|
||||
"heading": ""
|
||||
},
|
||||
"feedback": {
|
||||
"context": "",
|
||||
"cta": ""
|
||||
},
|
||||
"feedbackAlert": "",
|
||||
"forbidden": {
|
||||
"heading": ""
|
||||
},
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
"joinMeetingTipContent": "",
|
||||
"joinMeetingTipHeading": "",
|
||||
"loginToCreateMeeting": "",
|
||||
"moreLinkLabel": "",
|
||||
"moreLink": "",
|
||||
"moreAbout": "",
|
||||
"createMenu": {
|
||||
"laterOption": "",
|
||||
"instantOption": ""
|
||||
@@ -23,34 +20,5 @@
|
||||
"copy": "",
|
||||
"copied": "",
|
||||
"permissions": ""
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
},
|
||||
"beta": {
|
||||
"text": "",
|
||||
"tooltip": ""
|
||||
},
|
||||
"next": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
},
|
||||
"slide1": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"imgAlt": ""
|
||||
},
|
||||
"slide2": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"imgAlt": ""
|
||||
},
|
||||
"slide3": {
|
||||
"title": "",
|
||||
"body": "",
|
||||
"imgAlt": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"feedback": {
|
||||
"heading": "",
|
||||
"home": "",
|
||||
"back": ""
|
||||
"body": "",
|
||||
"heading": ""
|
||||
},
|
||||
"join": {
|
||||
"videoinput": {
|
||||
@@ -110,24 +109,6 @@
|
||||
"chat": {
|
||||
"disclaimer": ""
|
||||
},
|
||||
"rating": {
|
||||
"submit": "",
|
||||
"question": "",
|
||||
"levels": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
"openFeedback": {
|
||||
"question": "",
|
||||
"placeholder": "",
|
||||
"submit": "",
|
||||
"skip": ""
|
||||
},
|
||||
"confirmationMessage": {
|
||||
"heading": "",
|
||||
"body": ""
|
||||
},
|
||||
"participants": {
|
||||
"subheading": "",
|
||||
"contributors": "",
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
"error": {
|
||||
"heading": "An error occurred while loading the page"
|
||||
},
|
||||
"feedback": {
|
||||
"context": "Visio is still in early development — your input matters!",
|
||||
"cta": "Share your feedback"
|
||||
},
|
||||
"feedbackAlert": "Give us feedback",
|
||||
"forbidden": {
|
||||
"heading": "You don't have the permission to view this page"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"createMeeting": "Create a meeting",
|
||||
"heading": "Simple and Secure Video Conferencing",
|
||||
"intro": "Communicate and work with ease, without compromising your sovereignty",
|
||||
"heading": "Welcome in Visio",
|
||||
"intro": "Work easily, from anywhere.",
|
||||
"joinInputError": "Use a meeting link or code. Examples:",
|
||||
"joinInputExample": "URL or 10-letter code",
|
||||
"joinInputLabel": "Meeting link",
|
||||
@@ -10,9 +10,6 @@
|
||||
"joinMeetingTipContent": "You can join a meeting by pasting its full link in the browser's address bar.",
|
||||
"joinMeetingTipHeading": "Did you know?",
|
||||
"loginToCreateMeeting": "Login to create a meeting",
|
||||
"moreLinkLabel": "Learn more - new tab",
|
||||
"moreLink": "Learn more",
|
||||
"moreAbout": "about Visio",
|
||||
"createMenu": {
|
||||
"laterOption": "Create a meeting for a later date",
|
||||
"instantOption": "Start an instant meeting"
|
||||
@@ -23,34 +20,5 @@
|
||||
"copy": "Copy the meeting link",
|
||||
"copied": "Link copied to clipboard",
|
||||
"permissions": "People with this link do not need your permission to join this meeting."
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "previous",
|
||||
"tooltip": "previous"
|
||||
},
|
||||
"next": {
|
||||
"label": "next",
|
||||
"tooltip": "next"
|
||||
},
|
||||
"beta": {
|
||||
"text": "Join the beta",
|
||||
"tooltip": "Fill out the form"
|
||||
},
|
||||
"slide1": {
|
||||
"title": "Try Visio to simplify your daily tasks",
|
||||
"body": "Discover an intuitive and accessible solution, designed for all public agents, their partners, and much more.",
|
||||
"imgAlt": "Illustration of a user-friendly and accessible collaboration platform"
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Host group calls without limits",
|
||||
"body": "Unlimited time meetings, with smooth and high-quality communication, no matter the group size.",
|
||||
"imgAlt": "Image of a virtual meeting with multiple participants collaborating seamlessly"
|
||||
},
|
||||
"slide3": {
|
||||
"title": "Transform your meetings with AI",
|
||||
"body": "Get accurate and actionable transcripts to boost your productivity. Feature in beta—try it now!",
|
||||
"imgAlt": "Illustration of AI-powered note-taking in a virtual meeting"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"feedback": {
|
||||
"heading": "You have left the meeting",
|
||||
"home": "Return to home",
|
||||
"back": "Rejoin the meeting"
|
||||
"body": "Please fill out the form available in the header to give us your precious feedback! Thanks.",
|
||||
"heading": "Help us improve Visio"
|
||||
},
|
||||
"join": {
|
||||
"videoinput": {
|
||||
@@ -108,24 +107,6 @@
|
||||
"chat": {
|
||||
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
|
||||
},
|
||||
"rating": {
|
||||
"submit": "Submit",
|
||||
"question": "What do you think about the quality of your call?",
|
||||
"levels": {
|
||||
"min": "very poor",
|
||||
"max": "excellent"
|
||||
}
|
||||
},
|
||||
"openFeedback": {
|
||||
"question": "What can we do to improve Visio?",
|
||||
"placeholder": "Describe your bugs or share your suggestions…",
|
||||
"submit": "Submit",
|
||||
"skip": "Skip"
|
||||
},
|
||||
"confirmationMessage": {
|
||||
"heading": "Thank you for your submission",
|
||||
"body": "Our product team takes the time to carefully review your feedback. We will get back to you as soon as possible."
|
||||
},
|
||||
"participants": {
|
||||
"subheading": "In room",
|
||||
"you": "You",
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
"error": {
|
||||
"heading": "Une erreur est survenue lors du chargement de la page"
|
||||
},
|
||||
"feedback": {
|
||||
"context": "Visio est en pleine construction — votre avis compte !",
|
||||
"cta": "Partagez votre avis"
|
||||
},
|
||||
"feedbackAlert": "Donnez-nous votre avis",
|
||||
"forbidden": {
|
||||
"heading": "Accès interdit"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"createMeeting": "Créer une réunion",
|
||||
"heading": "Visioconférences simples et sécurisées",
|
||||
"intro": "Communiquez et travaillez en toute simplicité, sans compromis sur votre souveraineté",
|
||||
"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",
|
||||
"joinInputLabel": "Lien complet ou code de la réunion",
|
||||
@@ -10,9 +10,6 @@
|
||||
"joinMeetingTipContent": "Vous pouvez rejoindre une réunion en copiant directement son lien complet dans la barre d'adresse du navigateur.",
|
||||
"joinMeetingTipHeading": "Astuce",
|
||||
"loginToCreateMeeting": "Connectez-vous pour créer une réunion",
|
||||
"moreLinkLabel": "En savoir plus - nouvelle fenêtre",
|
||||
"moreLink": "En savoir plus",
|
||||
"moreAbout": "sur Visio",
|
||||
"createMenu": {
|
||||
"laterOption": "Créer une réunion pour une date ultérieure",
|
||||
"instantOption": "Démarrer une réunion instantanée"
|
||||
@@ -23,34 +20,5 @@
|
||||
"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."
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "précédent",
|
||||
"tooltip": "précédent"
|
||||
},
|
||||
"next": {
|
||||
"label": "suivant",
|
||||
"tooltip": "suivant"
|
||||
},
|
||||
"beta": {
|
||||
"text": "Essayer la beta",
|
||||
"tooltip": "Accéder au formulaire"
|
||||
},
|
||||
"slide1": {
|
||||
"title": "Essayez Visio pour simplifier votre quotidien",
|
||||
"body": "Découvrez une solution intuitive et accessible, conçue pour tous les agents publics et leurs partenaires, et bien plus encore.",
|
||||
"imgAlt": "Illustration d'une plateforme de collaboration simple et accessible"
|
||||
},
|
||||
"slide2": {
|
||||
"title": "Organisez des appels de groupe sans limite",
|
||||
"body": "Réunions sans limite de temps, avec une communication fluide et de haute qualité, quel que soit le nombre.",
|
||||
"imgAlt": "Image d'une réunion virtuelle avec plusieurs participants collaborant efficacement"
|
||||
},
|
||||
"slide3": {
|
||||
"title": "Transformez vos réunions avec l'IA",
|
||||
"body": "Obtenez des transcriptions précises et actionnables, pour booster votre productivité. Fonctionnalité en beta, essayez-la maintenant !",
|
||||
"imgAlt": "Illustration de prise de notes assistée par l'IA dans une réunion virtuelle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"feedback": {
|
||||
"heading": "Vous avez quitté la réunion",
|
||||
"home": "Retourner à l'accueil",
|
||||
"back": "Réintégrer la réunion"
|
||||
"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 Visio"
|
||||
},
|
||||
"join": {
|
||||
"videoinput": {
|
||||
@@ -108,24 +107,6 @@
|
||||
"chat": {
|
||||
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
|
||||
},
|
||||
"rating": {
|
||||
"submit": "Envoyer",
|
||||
"question": "Que pensez-vous de la qualité de votre appel ?",
|
||||
"levels": {
|
||||
"min": "très mauvaise",
|
||||
"max": "excellente"
|
||||
}
|
||||
},
|
||||
"openFeedback": {
|
||||
"question": "Que pouvons-nous faire pour améliorer Visio ?",
|
||||
"placeholder": "Décrivez vos bugs ou partagez vos suggestions …",
|
||||
"submit": "Envoyer",
|
||||
"skip": "Passer"
|
||||
},
|
||||
"confirmationMessage": {
|
||||
"heading": "Merci pour votre submission",
|
||||
"body": "Notre équipe produit prend le temps d'analyser attentivement vos réponses. Nous reviendrons vers vous dans les plus brefs délais."
|
||||
},
|
||||
"participants": {
|
||||
"subheading": "Dans la réunion",
|
||||
"you": "Vous",
|
||||
|
||||
@@ -9,7 +9,7 @@ const link = cva({
|
||||
borderRadius: 2,
|
||||
transition: 'all 0.2s',
|
||||
'&[data-hovered]': {
|
||||
textDecorationThickness: '2px',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
textDecoration: 'underline',
|
||||
|
||||
@@ -59,9 +59,6 @@ export const buttonRecipe = cva({
|
||||
},
|
||||
primary: {
|
||||
colorPalette: 'primary',
|
||||
'&[data-disabled]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
},
|
||||
// @TODO: better handling of colors… this is a mess
|
||||
success: {
|
||||
|
||||
@@ -28,4 +28,3 @@ export { Text } from './Text'
|
||||
export { ToggleButton } from './ToggleButton'
|
||||
export { Ul } from './Ul'
|
||||
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
||||
export { TextArea } from './TextArea'
|
||||
|
||||
@@ -24,7 +24,7 @@ backend:
|
||||
OIDC_RP_CLIENT_ID: {{ .Values.oidc.clientId }}
|
||||
OIDC_RP_CLIENT_SECRET: {{ .Values.oidc.clientSecret }}
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email given_name usual_name"
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet.127.0.0.1.nip.io
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
LOGIN_REDIRECT_URL: https://meet.127.0.0.1.nip.io
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
image:
|
||||
repository: lasuite/meet-backend
|
||||
pullPolicy: Always
|
||||
tag: "v0.1.8"
|
||||
tag: "v0.1.7"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
@@ -42,7 +42,7 @@ backend:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_SECRET
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email given_name usual_name"
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://visio.numerique.gouv.fr
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
LOGIN_REDIRECT_URL: https://visio.numerique.gouv.fr
|
||||
@@ -128,7 +128,7 @@ frontend:
|
||||
image:
|
||||
repository: lasuite/meet-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v0.1.8"
|
||||
tag: "v0.1.7"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@@ -41,7 +41,7 @@ backend:
|
||||
name: backend
|
||||
key: OIDC_RP_CLIENT_SECRET
|
||||
OIDC_RP_SIGN_ALGO: RS256
|
||||
OIDC_RP_SCOPES: "openid email given_name usual_name"
|
||||
OIDC_RP_SCOPES: "openid email"
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS: https://visio-staging.beta.numerique.gouv.fr
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
|
||||
LOGIN_REDIRECT_URL: https://visio-staging.beta.numerique.gouv.fr
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: core.libre.sh/v1alpha1
|
||||
kind: Bucket
|
||||
metadata:
|
||||
name: meet-media-storage
|
||||
name: impress-media-storage
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
provider: data
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.7",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user