Merge remote-tracking branch 'origin/livekit' into michaelk/various_testid_tags
@@ -21,5 +21,9 @@ server {
|
|||||||
expires 1w;
|
expires 1w;
|
||||||
add_header Cache-Control "public, no-transform";
|
add_header Cache-Control "public, no-transform";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /apple-app-site-association {
|
||||||
|
default_type application/json;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
"@sentry/tracing": "^6.13.3",
|
"@sentry/tracing": "^6.13.3",
|
||||||
"@use-gesture/react": "^10.2.11",
|
"@use-gesture/react": "^10.2.11",
|
||||||
"@vector-im/compound-design-tokens": "^0.0.5",
|
"@vector-im/compound-design-tokens": "^0.0.5",
|
||||||
"@vector-im/compound-web": "^0.2.15",
|
"@vector-im/compound-web": "^0.4.0",
|
||||||
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
||||||
"@vitejs/plugin-react": "^4.0.1",
|
"@vitejs/plugin-react": "^4.0.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"i18next-http-backend": "^1.4.4",
|
"i18next-http-backend": "^1.4.4",
|
||||||
"livekit-client": "^1.12.3",
|
"livekit-client": "^1.12.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#b698217445318f453e0b1086364a33113eaa85d9",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6836720e1e1c2cb01d49d6e5fcfc01afc14834ca",
|
||||||
"matrix-widget-api": "^1.3.1",
|
"matrix-widget-api": "^1.3.1",
|
||||||
"mermaid": "^9.0.0",
|
"mermaid": "^9.0.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
|||||||
12
public/.well-known/assetlinks.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "io.element.android.x.debug",
|
||||||
|
"sha256_cert_fingerprints": [
|
||||||
|
"B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
15
public/apple-app-site-association
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"applinks": {
|
||||||
|
"apps": [],
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"appIDs": [
|
||||||
|
"7J4U792NQT.io.element.elementx",
|
||||||
|
"7J4U792NQT.io.element.elementx.nightly",
|
||||||
|
"7J4U792NQT.io.element.elementx.pr"
|
||||||
|
],
|
||||||
|
"paths": ["*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
"Call link copied": "Връзка към разговора бе копирана",
|
"Call link copied": "Връзка към разговора бе копирана",
|
||||||
"Call type menu": "Меню \"тип на разговора\"",
|
"Call type menu": "Меню \"тип на разговора\"",
|
||||||
"Camera": "Камера",
|
"Camera": "Камера",
|
||||||
"Change layout": "Промени изгледа",
|
|
||||||
"Close": "Затвори",
|
"Close": "Затвори",
|
||||||
"Confirm password": "Потвърди паролата",
|
"Confirm password": "Потвърди паролата",
|
||||||
"Copied!": "Копирано!",
|
"Copied!": "Копирано!",
|
||||||
@@ -16,27 +15,20 @@
|
|||||||
"Create account": "Създай акаунт",
|
"Create account": "Създай акаунт",
|
||||||
"Debug log": "Debug логове",
|
"Debug log": "Debug логове",
|
||||||
"Debug log request": "Заявка за debug логове",
|
"Debug log request": "Заявка за debug логове",
|
||||||
"Details": "Детайли",
|
|
||||||
"Developer": "Разработчик",
|
"Developer": "Разработчик",
|
||||||
"Display name": "Име/псевдоним",
|
"Display name": "Име/псевдоним",
|
||||||
"Download debug logs": "Изтеглете debug логове",
|
"Download debug logs": "Изтеглете debug логове",
|
||||||
"Exit full screen": "Излез от цял екран",
|
"Exit full screen": "Излез от цял екран",
|
||||||
"Fetching group call timed out.": "Изтече времето за взимане на груповия разговор.",
|
"Fetching group call timed out.": "Изтече времето за взимане на груповия разговор.",
|
||||||
"Freedom": "Свобода",
|
|
||||||
"Full screen": "Цял екран",
|
"Full screen": "Цял екран",
|
||||||
"Go": "Напред",
|
"Go": "Напред",
|
||||||
"Grid layout menu": "Меню \"решетков изглед\"",
|
|
||||||
"Home": "Начало",
|
"Home": "Начало",
|
||||||
"Include debug logs": "Включи debug логове",
|
"Include debug logs": "Включи debug логове",
|
||||||
"Incompatible versions": "Несъвместими версии",
|
"Incompatible versions": "Несъвместими версии",
|
||||||
"Incompatible versions!": "Несъвместими версии!",
|
|
||||||
"Inspector": "Инспектор",
|
"Inspector": "Инспектор",
|
||||||
"Invite": "Покани",
|
|
||||||
"Invite people": "Покани хора",
|
|
||||||
"Join call": "Влез в разговора",
|
"Join call": "Влез в разговора",
|
||||||
"Join call now": "Влез в разговора сега",
|
"Join call now": "Влез в разговора сега",
|
||||||
"Join existing call?": "Присъединяване към съществуващ разговор?",
|
"Join existing call?": "Присъединяване към съществуващ разговор?",
|
||||||
"Leave": "Напусни",
|
|
||||||
"Loading…": "Зареждане…",
|
"Loading…": "Зареждане…",
|
||||||
"Local volume": "Локална сила на звука",
|
"Local volume": "Локална сила на звука",
|
||||||
"Logging in…": "Влизане…",
|
"Logging in…": "Влизане…",
|
||||||
@@ -44,7 +36,6 @@
|
|||||||
"Login to your account": "Влезте в акаунта си",
|
"Login to your account": "Влезте в акаунта си",
|
||||||
"Microphone": "Микрофон",
|
"Microphone": "Микрофон",
|
||||||
"More": "Още",
|
"More": "Още",
|
||||||
"Mute microphone": "Заглуши микрофона",
|
|
||||||
"No": "Не",
|
"No": "Не",
|
||||||
"Not now, return to home screen": "Не сега, върни се на началния екран",
|
"Not now, return to home screen": "Не сега, върни се на началния екран",
|
||||||
"Not registered yet? <2>Create an account</2>": "Все още не сте регистрирани? <2>Създайте акаунт</2>",
|
"Not registered yet? <2>Create an account</2>": "Все още не сте регистрирани? <2>Създайте акаунт</2>",
|
||||||
@@ -68,13 +59,9 @@
|
|||||||
"Sign out": "Излез",
|
"Sign out": "Излез",
|
||||||
"Speaker": "Говорител",
|
"Speaker": "Говорител",
|
||||||
"Spotlight": "Прожектор",
|
"Spotlight": "Прожектор",
|
||||||
"Stop sharing screen": "Спри споделянето на екрана",
|
|
||||||
"Submit feedback": "Изпрати обратна връзка",
|
"Submit feedback": "Изпрати обратна връзка",
|
||||||
"Take me Home": "Отиди в Начало",
|
"Take me Home": "Отиди в Начало",
|
||||||
"This call already exists, would you like to join?": "Този разговор вече съществува, искате ли да се присъедините?",
|
"This call already exists, would you like to join?": "Този разговор вече съществува, искате ли да се присъедините?",
|
||||||
"Turn off camera": "Изключи камерата",
|
|
||||||
"Turn on camera": "Включи камерата",
|
|
||||||
"Unmute microphone": "Включи микрофона",
|
|
||||||
"User menu": "Потребителско меню",
|
"User menu": "Потребителско меню",
|
||||||
"Username": "Потребителско име",
|
"Username": "Потребителско име",
|
||||||
"Version: {{version}}": "Версия: {{version}}",
|
"Version: {{version}}": "Версия: {{version}}",
|
||||||
@@ -86,6 +73,5 @@
|
|||||||
"Walkie-talkie call name": "Име на уоки-токи разговора",
|
"Walkie-talkie call name": "Име на уоки-токи разговора",
|
||||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не се поддържа или се блокира от браузъра.",
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не се поддържа или се блокира от браузъра.",
|
||||||
"Yes, join call": "Да, присъедини се",
|
"Yes, join call": "Да, присъедини се",
|
||||||
"Your recent calls": "Скорошните ви разговори",
|
"Your recent calls": "Скорошните ви разговори"
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"Copied!": "Zkopírováno!",
|
"Copied!": "Zkopírováno!",
|
||||||
"Confirm password": "Potvrdit heslo",
|
"Confirm password": "Potvrdit heslo",
|
||||||
"Close": "Zavřít",
|
"Close": "Zavřít",
|
||||||
"Change layout": "Změnit rozložení",
|
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
"Call link copied": "Odkaz na hovor zkopírován",
|
"Call link copied": "Odkaz na hovor zkopírován",
|
||||||
"Avatar": "Avatar",
|
"Avatar": "Avatar",
|
||||||
@@ -19,13 +18,9 @@
|
|||||||
"Version: {{version}}": "Verze: {{version}}",
|
"Version: {{version}}": "Verze: {{version}}",
|
||||||
"Username": "Uživatelské jméno",
|
"Username": "Uživatelské jméno",
|
||||||
"User menu": "Uživatelské menu",
|
"User menu": "Uživatelské menu",
|
||||||
"Unmute microphone": "Zapnout mikrofon",
|
|
||||||
"Turn on camera": "Zapnout kameru",
|
|
||||||
"Turn off camera": "Vypnout kameru",
|
|
||||||
"This call already exists, would you like to join?": "Tento hovor již existuje, chcete se připojit?",
|
"This call already exists, would you like to join?": "Tento hovor již existuje, chcete se připojit?",
|
||||||
"Take me Home": "Domovská obrazovka",
|
"Take me Home": "Domovská obrazovka",
|
||||||
"Submit feedback": "Dát feedback",
|
"Submit feedback": "Dát feedback",
|
||||||
"Stop sharing screen": "Zastavit sdílení obrazovek",
|
|
||||||
"Speaker": "Reproduktor",
|
"Speaker": "Reproduktor",
|
||||||
"Sign out": "Odhlásit se",
|
"Sign out": "Odhlásit se",
|
||||||
"Sign in": "Přihlásit se",
|
"Sign in": "Přihlásit se",
|
||||||
@@ -45,7 +40,6 @@
|
|||||||
"Password": "Heslo",
|
"Password": "Heslo",
|
||||||
"Not now, return to home screen": "Teď ne, vrátit se na domovskou obrazovku",
|
"Not now, return to home screen": "Teď ne, vrátit se na domovskou obrazovku",
|
||||||
"No": "Ne",
|
"No": "Ne",
|
||||||
"Mute microphone": "Ztlumit mikrofon",
|
|
||||||
"More": "Více",
|
"More": "Více",
|
||||||
"Microphone": "Mikrofon",
|
"Microphone": "Mikrofon",
|
||||||
"Login to your account": "Přihlásit se ke svému účtu",
|
"Login to your account": "Přihlásit se ke svému účtu",
|
||||||
@@ -53,17 +47,12 @@
|
|||||||
"Logging in…": "Přihlašování se…",
|
"Logging in…": "Přihlašování se…",
|
||||||
"Local volume": "Lokální hlasitost",
|
"Local volume": "Lokální hlasitost",
|
||||||
"Loading…": "Načítání…",
|
"Loading…": "Načítání…",
|
||||||
"Leave": "Opustit hovor",
|
|
||||||
"Join call now": "Připojit se k hovoru",
|
"Join call now": "Připojit se k hovoru",
|
||||||
"Join call": "Připojit se k hovoru",
|
"Join call": "Připojit se k hovoru",
|
||||||
"Invite people": "Pozvat lidi",
|
|
||||||
"Invite": "Pozvat",
|
|
||||||
"Inspector": "Insepktor",
|
"Inspector": "Insepktor",
|
||||||
"Incompatible versions!": "Nekompatibilní verze!",
|
|
||||||
"Incompatible versions": "Nekompatibilní verze",
|
"Incompatible versions": "Nekompatibilní verze",
|
||||||
"Walkie-talkie call name": "Jméno vysílačkového hovoru",
|
"Walkie-talkie call name": "Jméno vysílačkového hovoru",
|
||||||
"Walkie-talkie call": "Vysílačkový hovor",
|
"Walkie-talkie call": "Vysílačkový hovor",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Spotlight": "Soustředěný mód",
|
"Spotlight": "Soustředěný mód",
|
||||||
"Recaptcha not loaded": "Recaptcha se nenačetla",
|
"Recaptcha not loaded": "Recaptcha se nenačetla",
|
||||||
"Recaptcha dismissed": "Recaptcha byla zamítnuta",
|
"Recaptcha dismissed": "Recaptcha byla zamítnuta",
|
||||||
@@ -72,17 +61,14 @@
|
|||||||
"Join existing call?": "Připojit se k existujícimu hovoru?",
|
"Join existing call?": "Připojit se k existujícimu hovoru?",
|
||||||
"Include debug logs": "Zahrnout ladící záznamy",
|
"Include debug logs": "Zahrnout ladící záznamy",
|
||||||
"Home": "Domov",
|
"Home": "Domov",
|
||||||
"Grid layout menu": "Menu rozložení",
|
|
||||||
"Go": "Pokračovat",
|
"Go": "Pokračovat",
|
||||||
"Full screen": "Zvětšit na celou obrazovku",
|
"Full screen": "Zvětšit na celou obrazovku",
|
||||||
"Freedom": "Volný",
|
|
||||||
"Fetching group call timed out.": "Vypršel časový limit načítání skupinového hovoru.",
|
"Fetching group call timed out.": "Vypršel časový limit načítání skupinového hovoru.",
|
||||||
"Exit full screen": "Ukončit režim celé obrazovky",
|
"Exit full screen": "Ukončit režim celé obrazovky",
|
||||||
"Element Call Home": "Domov Element Call",
|
"Element Call Home": "Domov Element Call",
|
||||||
"Download debug logs": "Stáhnout ladící záznamy",
|
"Download debug logs": "Stáhnout ladící záznamy",
|
||||||
"Display name": "Zobrazované jméno",
|
"Display name": "Zobrazované jméno",
|
||||||
"Developer": "Vývojář",
|
"Developer": "Vývojář",
|
||||||
"Details": "Detaily",
|
|
||||||
"Debug log request": "Žádost o protokoly ladění",
|
"Debug log request": "Žádost o protokoly ladění",
|
||||||
"Debug log": "Protokoly ladění",
|
"Debug log": "Protokoly ladění",
|
||||||
"Create account": "Vytvořit účet",
|
"Create account": "Vytvořit účet",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"Call link copied": "Anruflink kopiert",
|
"Call link copied": "Anruflink kopiert",
|
||||||
"Call type menu": "Anruftyp Menü",
|
"Call type menu": "Anruftyp Menü",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
"Change layout": "Layout ändern",
|
|
||||||
"Close": "Schließen",
|
"Close": "Schließen",
|
||||||
"Confirm password": "Passwort bestätigen",
|
"Confirm password": "Passwort bestätigen",
|
||||||
"Copied!": "Kopiert!",
|
"Copied!": "Kopiert!",
|
||||||
@@ -16,26 +15,19 @@
|
|||||||
"Create account": "Konto erstellen",
|
"Create account": "Konto erstellen",
|
||||||
"Debug log": "Debug-Protokoll",
|
"Debug log": "Debug-Protokoll",
|
||||||
"Debug log request": "Debug-Log Anfrage",
|
"Debug log request": "Debug-Log Anfrage",
|
||||||
"Details": "Details",
|
|
||||||
"Developer": "Entwickler",
|
"Developer": "Entwickler",
|
||||||
"Display name": "Anzeigename",
|
"Display name": "Anzeigename",
|
||||||
"Download debug logs": "Debug-Protokolle herunterladen",
|
"Download debug logs": "Debug-Protokolle herunterladen",
|
||||||
"Exit full screen": "Vollbildmodus verlassen",
|
"Exit full screen": "Vollbildmodus verlassen",
|
||||||
"Freedom": "Freiraum",
|
|
||||||
"Full screen": "Vollbild",
|
"Full screen": "Vollbild",
|
||||||
"Go": "Los geht’s",
|
"Go": "Los geht’s",
|
||||||
"Grid layout menu": "Grid-Layout-Menü",
|
|
||||||
"Home": "Startseite",
|
"Home": "Startseite",
|
||||||
"Include debug logs": "Debug-Protokolle einschließen",
|
"Include debug logs": "Debug-Protokolle einschließen",
|
||||||
"Incompatible versions": "Inkompatible Versionen",
|
"Incompatible versions": "Inkompatible Versionen",
|
||||||
"Incompatible versions!": "Inkompatible Versionen!",
|
|
||||||
"Inspector": "Inspektor",
|
"Inspector": "Inspektor",
|
||||||
"Invite": "Einladen",
|
|
||||||
"Invite people": "Personen einladen",
|
|
||||||
"Join call": "Anruf beitreten",
|
"Join call": "Anruf beitreten",
|
||||||
"Join call now": "Anruf beitreten",
|
"Join call now": "Anruf beitreten",
|
||||||
"Join existing call?": "An bestehendem Anruf teilnehmen?",
|
"Join existing call?": "An bestehendem Anruf teilnehmen?",
|
||||||
"Leave": "Verlassen",
|
|
||||||
"Loading…": "Lade …",
|
"Loading…": "Lade …",
|
||||||
"Local volume": "Lokale Lautstärke",
|
"Local volume": "Lokale Lautstärke",
|
||||||
"Logging in…": "Anmelden …",
|
"Logging in…": "Anmelden …",
|
||||||
@@ -43,7 +35,6 @@
|
|||||||
"Login to your account": "Melde dich mit deinem Konto an",
|
"Login to your account": "Melde dich mit deinem Konto an",
|
||||||
"Microphone": "Mikrofon",
|
"Microphone": "Mikrofon",
|
||||||
"More": "Mehr",
|
"More": "Mehr",
|
||||||
"Mute microphone": "Mikrofon stummschalten",
|
|
||||||
"No": "Nein",
|
"No": "Nein",
|
||||||
"Not now, return to home screen": "Nicht jetzt, zurück zum Startbildschirm",
|
"Not now, return to home screen": "Nicht jetzt, zurück zum Startbildschirm",
|
||||||
"Not registered yet? <2>Create an account</2>": "Noch nicht registriert? <2>Konto erstellen</2>",
|
"Not registered yet? <2>Create an account</2>": "Noch nicht registriert? <2>Konto erstellen</2>",
|
||||||
@@ -67,13 +58,9 @@
|
|||||||
"Sign out": "Abmelden",
|
"Sign out": "Abmelden",
|
||||||
"Speaker": "Wiedergabegerät",
|
"Speaker": "Wiedergabegerät",
|
||||||
"Spotlight": "Rampenlicht",
|
"Spotlight": "Rampenlicht",
|
||||||
"Stop sharing screen": "Beenden der Bildschirmfreigabe",
|
|
||||||
"Submit feedback": "Rückmeldung geben",
|
"Submit feedback": "Rückmeldung geben",
|
||||||
"Take me Home": "Zurück zur Startseite",
|
"Take me Home": "Zurück zur Startseite",
|
||||||
"This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?",
|
"This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?",
|
||||||
"Turn off camera": "Kamera ausschalten",
|
|
||||||
"Turn on camera": "Kamera einschalten",
|
|
||||||
"Unmute microphone": "Mikrofon aktivieren",
|
|
||||||
"User menu": "Benutzermenü",
|
"User menu": "Benutzermenü",
|
||||||
"Username": "Benutzername",
|
"Username": "Benutzername",
|
||||||
"Version: {{version}}": "Version: {{version}}",
|
"Version: {{version}}": "Version: {{version}}",
|
||||||
@@ -85,7 +72,6 @@
|
|||||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC wird in diesem Browser nicht unterstützt oder ist blockiert.",
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC wird in diesem Browser nicht unterstützt oder ist blockiert.",
|
||||||
"Yes, join call": "Ja, Anruf beitreten",
|
"Yes, join call": "Ja, Anruf beitreten",
|
||||||
"Your recent calls": "Deine letzten Anrufe",
|
"Your recent calls": "Deine letzten Anrufe",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Fetching group call timed out.": "Zeitüberschreitung beim Abrufen des Gruppenanrufs.",
|
"Fetching group call timed out.": "Zeitüberschreitung beim Abrufen des Gruppenanrufs.",
|
||||||
"Walkie-talkie call name": "Name des Walkie-Talkie-Anrufs",
|
"Walkie-talkie call name": "Name des Walkie-Talkie-Anrufs",
|
||||||
"Sending debug logs…": "Sende Debug-Protokolle …",
|
"Sending debug logs…": "Sende Debug-Protokolle …",
|
||||||
@@ -118,7 +104,6 @@
|
|||||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call ist temporär nicht Ende-zu-Ende-verschlüsselt, während wir die Skalierbarkeit testen.",
|
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call ist temporär nicht Ende-zu-Ende-verschlüsselt, während wir die Skalierbarkeit testen.",
|
||||||
"Connectivity to the server has been lost.": "Die Verbindung zum Server wurde getrennt.",
|
"Connectivity to the server has been lost.": "Die Verbindung zum Server wurde getrennt.",
|
||||||
"Enable end-to-end encryption (password protected calls)": "Ende-zu-Ende-Verschlüsselung aktivieren (Passwort geschützte Anrufe)",
|
"Enable end-to-end encryption (password protected calls)": "Ende-zu-Ende-Verschlüsselung aktivieren (Passwort geschützte Anrufe)",
|
||||||
"Password (if none, E2EE is disabled)": "Passwort (falls leer, wird E2EE deaktiviert)",
|
|
||||||
"End-to-end encryption isn't supported on your browser.": "Ende-zu-Ende-Verschlüsselung wird in deinem Browser nicht unterstützt.",
|
"End-to-end encryption isn't supported on your browser.": "Ende-zu-Ende-Verschlüsselung wird in deinem Browser nicht unterstützt.",
|
||||||
"Thanks!": "Danke!",
|
"Thanks!": "Danke!",
|
||||||
"You were disconnected from the call": "Deine Verbindung wurde getrennt",
|
"You were disconnected from the call": "Deine Verbindung wurde getrennt",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"Version: {{version}}": "Έκδοση: {{version}}",
|
"Version: {{version}}": "Έκδοση: {{version}}",
|
||||||
"User menu": "Μενού χρήστη",
|
"User menu": "Μενού χρήστη",
|
||||||
"Submit feedback": "Υποβάλετε σχόλια",
|
"Submit feedback": "Υποβάλετε σχόλια",
|
||||||
"Stop sharing screen": "Διακοπή κοινής χρήσης οθόνης",
|
|
||||||
"Sign in": "Σύνδεση",
|
"Sign in": "Σύνδεση",
|
||||||
"Show call inspector": "Εμφάνιση του επιθεωρητή κλήσης",
|
"Show call inspector": "Εμφάνιση του επιθεωρητή κλήσης",
|
||||||
"Share screen": "Κοινή χρήση οθόνης",
|
"Share screen": "Κοινή χρήση οθόνης",
|
||||||
@@ -13,10 +12,7 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "Δεν έχετε εγγραφεί ακόμα; <2>Δημιουργήστε λογαριασμό</2>",
|
"Not registered yet? <2>Create an account</2>": "Δεν έχετε εγγραφεί ακόμα; <2>Δημιουργήστε λογαριασμό</2>",
|
||||||
"Login to your account": "Συνδεθείτε στον λογαριασμό σας",
|
"Login to your account": "Συνδεθείτε στον λογαριασμό σας",
|
||||||
"Logging in…": "Σύνδεση…",
|
"Logging in…": "Σύνδεση…",
|
||||||
"Invite people": "Προσκαλέστε άτομα",
|
|
||||||
"Invite": "Πρόσκληση",
|
|
||||||
"Inspector": "Επιθεωρητής",
|
"Inspector": "Επιθεωρητής",
|
||||||
"Incompatible versions!": "Μη συμβατές εκδόσεις!",
|
|
||||||
"Incompatible versions": "Μη συμβατές εκδόσεις",
|
"Incompatible versions": "Μη συμβατές εκδόσεις",
|
||||||
"Display name": "Εμφανιζόμενο όνομα",
|
"Display name": "Εμφανιζόμενο όνομα",
|
||||||
"Developer Settings": "Ρυθμίσεις προγραμματιστή",
|
"Developer Settings": "Ρυθμίσεις προγραμματιστή",
|
||||||
@@ -26,7 +22,6 @@
|
|||||||
"<0>Oops, something's gone wrong.</0>": "<0>Ωχ, κάτι πήγε στραβά.</0>",
|
"<0>Oops, something's gone wrong.</0>": "<0>Ωχ, κάτι πήγε στραβά.</0>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Δημιουργήστε λογαριασμό</0> Ή <2>Συμμετέχετε ως επισκέπτης</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Δημιουργήστε λογαριασμό</0> Ή <2>Συμμετέχετε ως επισκέπτης</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Έχετε ήδη λογαριασμό;</0><1><0>Συνδεθείτε</0> Ή <2>Συμμετέχετε ως επισκέπτης</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Έχετε ήδη λογαριασμό;</0><1><0>Συνδεθείτε</0> Ή <2>Συμμετέχετε ως επισκέπτης</2></1>",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Your recent calls": "Οι πρόσφατες κλήσεις σας",
|
"Your recent calls": "Οι πρόσφατες κλήσεις σας",
|
||||||
"Yes, join call": "Ναι, συμμετοχή στην κλήση",
|
"Yes, join call": "Ναι, συμμετοχή στην κλήση",
|
||||||
"WebRTC is not supported or is being blocked in this browser.": "Το WebRTC δεν υποστηρίζεται ή έχει αποκλειστεί σε αυτό το πρόγραμμα περιήγησης.",
|
"WebRTC is not supported or is being blocked in this browser.": "Το WebRTC δεν υποστηρίζεται ή έχει αποκλειστεί σε αυτό το πρόγραμμα περιήγησης.",
|
||||||
@@ -37,8 +32,6 @@
|
|||||||
"Video call": "Βίντεο κλήση",
|
"Video call": "Βίντεο κλήση",
|
||||||
"Video": "Βίντεο",
|
"Video": "Βίντεο",
|
||||||
"Username": "Όνομα χρήστη",
|
"Username": "Όνομα χρήστη",
|
||||||
"Turn on camera": "Ενεργοποιήστε την κάμερα",
|
|
||||||
"Turn off camera": "Απενεργοποιήστε την κάμερα",
|
|
||||||
"This call already exists, would you like to join?": "Αυτή η κλήση υπάρχει ήδη, θα θέλατε να συμμετάσχετε;",
|
"This call already exists, would you like to join?": "Αυτή η κλήση υπάρχει ήδη, θα θέλατε να συμμετάσχετε;",
|
||||||
"Speaker": "Ηχείο",
|
"Speaker": "Ηχείο",
|
||||||
"Sign out": "Αποσύνδεση",
|
"Sign out": "Αποσύνδεση",
|
||||||
@@ -50,26 +43,22 @@
|
|||||||
"Password": "Κωδικός",
|
"Password": "Κωδικός",
|
||||||
"Not now, return to home screen": "Όχι τώρα, επιστροφή στην αρχική οθόνη",
|
"Not now, return to home screen": "Όχι τώρα, επιστροφή στην αρχική οθόνη",
|
||||||
"No": "Όχι",
|
"No": "Όχι",
|
||||||
"Mute microphone": "Σίγαση μικροφώνου",
|
|
||||||
"More": "Περισσότερα",
|
"More": "Περισσότερα",
|
||||||
"Microphone": "Μικρόφωνο",
|
"Microphone": "Μικρόφωνο",
|
||||||
"Login": "Σύνδεση",
|
"Login": "Σύνδεση",
|
||||||
"Loading…": "Φόρτωση…",
|
"Loading…": "Φόρτωση…",
|
||||||
"Leave": "Αποχώρηση",
|
|
||||||
"Join existing call?": "Συμμετοχή στην υπάρχουσα κλήση;",
|
"Join existing call?": "Συμμετοχή στην υπάρχουσα κλήση;",
|
||||||
"Join call now": "Συμμετοχή στην κλήση τώρα",
|
"Join call now": "Συμμετοχή στην κλήση τώρα",
|
||||||
"Join call": "Συμμετοχή στην κλήση",
|
"Join call": "Συμμετοχή στην κλήση",
|
||||||
"Go": "Μετάβαση",
|
"Go": "Μετάβαση",
|
||||||
"Full screen": "Πλήρη οθόνη",
|
"Full screen": "Πλήρη οθόνη",
|
||||||
"Exit full screen": "Έξοδος από πλήρη οθόνη",
|
"Exit full screen": "Έξοδος από πλήρη οθόνη",
|
||||||
"Details": "Λεπτομέρειες",
|
|
||||||
"Create account": "Δημιουργία λογαριασμού",
|
"Create account": "Δημιουργία λογαριασμού",
|
||||||
"Copy and share this call link": "Αντιγράψτε και μοιραστείτε αυτόν τον σύνδεσμο κλήσης",
|
"Copy and share this call link": "Αντιγράψτε και μοιραστείτε αυτόν τον σύνδεσμο κλήσης",
|
||||||
"Copy": "Αντιγραφή",
|
"Copy": "Αντιγραφή",
|
||||||
"Copied!": "Αντιγράφηκε!",
|
"Copied!": "Αντιγράφηκε!",
|
||||||
"Confirm password": "Επιβεβαίωση κωδικού",
|
"Confirm password": "Επιβεβαίωση κωδικού",
|
||||||
"Close": "Κλείσιμο",
|
"Close": "Κλείσιμο",
|
||||||
"Change layout": "Αλλαγή διάταξης",
|
|
||||||
"Camera": "Κάμερα",
|
"Camera": "Κάμερα",
|
||||||
"Audio": "Ήχος",
|
"Audio": "Ήχος",
|
||||||
"Send debug logs": "Αποστολή αρχείων καταγραφής",
|
"Send debug logs": "Αποστολή αρχείων καταγραφής",
|
||||||
@@ -79,7 +68,6 @@
|
|||||||
"Local volume": "Τοπική ένταση",
|
"Local volume": "Τοπική ένταση",
|
||||||
"Home": "Αρχική",
|
"Home": "Αρχική",
|
||||||
"Show connection stats": "Εμφάνιση στατιστικών σύνδεσης",
|
"Show connection stats": "Εμφάνιση στατιστικών σύνδεσης",
|
||||||
"Unmute microphone": "Κατάργηση σίγασης μικροφώνου",
|
|
||||||
"Take me Home": "Μετάβαση στην Αρχική",
|
"Take me Home": "Μετάβαση στην Αρχική",
|
||||||
"{{displayName}} is presenting": "{{displayName}} παρουσιάζει",
|
"{{displayName}} is presenting": "{{displayName}} παρουσιάζει",
|
||||||
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.",
|
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.",
|
||||||
@@ -88,7 +76,6 @@
|
|||||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Γιατί να μην ολοκληρώσετε με τη δημιουργία ενός κωδικού πρόσβασης για τη διατήρηση του λογαριασμού σας;</0><1>Θα μπορείτε να διατηρήσετε το όνομά σας και να ορίσετε ένα avatar για χρήση σε μελλοντικές κλήσεις.</1>",
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Γιατί να μην ολοκληρώσετε με τη δημιουργία ενός κωδικού πρόσβασης για τη διατήρηση του λογαριασμού σας;</0><1>Θα μπορείτε να διατηρήσετε το όνομά σας και να ορίσετε ένα avatar για χρήση σε μελλοντικές κλήσεις.</1>",
|
||||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ένας άλλος χρήστης σε αυτή την κλήση έχει ένα πρόβλημα. Για την καλύτερη διάγνωση αυτών των προβλημάτων θα θέλαμε να συλλέξουμε ένα αρχείο καταγραφής σφαλμάτων.",
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ένας άλλος χρήστης σε αυτή την κλήση έχει ένα πρόβλημα. Για την καλύτερη διάγνωση αυτών των προβλημάτων θα θέλαμε να συλλέξουμε ένα αρχείο καταγραφής σφαλμάτων.",
|
||||||
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Συμμετέχοντας σε αυτή τη δοκιμαστική έκδοση, συναινείτε στη συλλογή ανώνυμων δεδομένων, τα οποία χρησιμοποιούμε για τη βελτίωση του προϊόντος. Μπορείτε να βρείτε περισσότερες πληροφορίες σχετικά με το ποια δεδομένα καταγράφουμε στην <2>Πολιτική απορρήτου</2> και στην <5>Πολιτική cookies</5>.",
|
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Συμμετέχοντας σε αυτή τη δοκιμαστική έκδοση, συναινείτε στη συλλογή ανώνυμων δεδομένων, τα οποία χρησιμοποιούμε για τη βελτίωση του προϊόντος. Μπορείτε να βρείτε περισσότερες πληροφορίες σχετικά με το ποια δεδομένα καταγράφουμε στην <2>Πολιτική απορρήτου</2> και στην <5>Πολιτική cookies</5>.",
|
||||||
"Grid layout menu": "Μενού διάταξης πλέγματος",
|
|
||||||
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.",
|
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.",
|
||||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Κάποιοι άλλοι χρήστες προσπαθούν να συμμετάσχουν σε αυτή την κλήση από ασύμβατες εκδόσεις. Αυτοί οι χρήστες θα πρέπει να βεβαιωθούν ότι έχουν κάνει ανανέωση (refresh) την καρτέλα του περιηγητή τους:<1>{userLis}</1>",
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Κάποιοι άλλοι χρήστες προσπαθούν να συμμετάσχουν σε αυτή την κλήση από ασύμβατες εκδόσεις. Αυτοί οι χρήστες θα πρέπει να βεβαιωθούν ότι έχουν κάνει ανανέωση (refresh) την καρτέλα του περιηγητή τους:<1>{userLis}</1>",
|
||||||
"Expose developer settings in the settings window.": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.",
|
"Expose developer settings in the settings window.": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.",
|
||||||
@@ -109,7 +96,6 @@
|
|||||||
"Submit": "Υποβολή",
|
"Submit": "Υποβολή",
|
||||||
"Your feedback": "Τα σχόλιά σας",
|
"Your feedback": "Τα σχόλιά σας",
|
||||||
"Fetching group call timed out.": "Η ομαδική κλήση έληξε από τέλος χρόνου.",
|
"Fetching group call timed out.": "Η ομαδική κλήση έληξε από τέλος χρόνου.",
|
||||||
"Freedom": "Ελευθερία",
|
|
||||||
"Spotlight": "Spotlight",
|
"Spotlight": "Spotlight",
|
||||||
"Element Call Home": "Element Κεντρική Οθόνη Κλήσεων"
|
"Element Call Home": "Element Κεντρική Οθόνη Κλήσεων"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"{{count}} stars|one": "{{count}} star",
|
"{{count, number}}|one": "{{count, number}}",
|
||||||
|
"{{count, number}}|other": "{{count, number}}",
|
||||||
|
"{{count}} stars|one": "{{count}} stars",
|
||||||
"{{count}} stars|other": "{{count}} stars",
|
"{{count}} stars|other": "{{count}} stars",
|
||||||
"{{displayName}} is presenting": "{{displayName}} is presenting",
|
"{{displayName}} is presenting": "{{displayName}} is presenting",
|
||||||
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
|
"{{displayName}}, your call has ended.": "{{displayName}}, your call has ended.",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}",
|
||||||
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
|
"<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
|
||||||
@@ -22,7 +24,6 @@
|
|||||||
"Call link copied": "Call link copied",
|
"Call link copied": "Call link copied",
|
||||||
"Call type menu": "Call type menu",
|
"Call type menu": "Call type menu",
|
||||||
"Camera": "Camera",
|
"Camera": "Camera",
|
||||||
"Change layout": "Change layout",
|
|
||||||
"Close": "Close",
|
"Close": "Close",
|
||||||
"Confirm password": "Confirm password",
|
"Confirm password": "Confirm password",
|
||||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||||
@@ -32,7 +33,6 @@
|
|||||||
"Create account": "Create account",
|
"Create account": "Create account",
|
||||||
"Debug log": "Debug log",
|
"Debug log": "Debug log",
|
||||||
"Debug log request": "Debug log request",
|
"Debug log request": "Debug log request",
|
||||||
"Details": "Details",
|
|
||||||
"Developer": "Developer",
|
"Developer": "Developer",
|
||||||
"Developer Settings": "Developer Settings",
|
"Developer Settings": "Developer Settings",
|
||||||
"Display name": "Display name",
|
"Display name": "Display name",
|
||||||
@@ -40,25 +40,20 @@
|
|||||||
"Element Call Home": "Element Call Home",
|
"Element Call Home": "Element Call Home",
|
||||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
|
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
|
||||||
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
|
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
|
||||||
|
"Encrypted": "Encrypted",
|
||||||
"End call": "End call",
|
"End call": "End call",
|
||||||
"End-to-end encryption isn't supported on your browser.": "End-to-end encryption isn't supported on your browser.",
|
"End-to-end encryption isn't supported on your browser.": "End-to-end encryption isn't supported on your browser.",
|
||||||
"Exit full screen": "Exit full screen",
|
"Exit full screen": "Exit full screen",
|
||||||
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
|
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
|
||||||
"Feedback": "Feedback",
|
"Feedback": "Feedback",
|
||||||
"Fetching group call timed out.": "Fetching group call timed out.",
|
|
||||||
"Freedom": "Freedom",
|
|
||||||
"Full screen": "Full screen",
|
"Full screen": "Full screen",
|
||||||
"Go": "Go",
|
"Go": "Go",
|
||||||
"Grid layout menu": "Grid layout menu",
|
"Grid": "Grid",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
"How did it go?": "How did it go?",
|
"How did it go?": "How did it go?",
|
||||||
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
||||||
"Include debug logs": "Include debug logs",
|
"Include debug logs": "Include debug logs",
|
||||||
"Incompatible versions": "Incompatible versions",
|
|
||||||
"Incompatible versions!": "Incompatible versions!",
|
|
||||||
"Inspector": "Inspector",
|
"Inspector": "Inspector",
|
||||||
"Invite": "Invite",
|
|
||||||
"Invite people": "Invite people",
|
|
||||||
"Join call": "Join call",
|
"Join call": "Join call",
|
||||||
"Join call now": "Join call now",
|
"Join call now": "Join call now",
|
||||||
"Join existing call?": "Join existing call?",
|
"Join existing call?": "Join existing call?",
|
||||||
@@ -72,9 +67,9 @@
|
|||||||
"Microphone on": "Microphone on",
|
"Microphone on": "Microphone on",
|
||||||
"More": "More",
|
"More": "More",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
|
"Not encrypted": "Not encrypted",
|
||||||
"Not now, return to home screen": "Not now, return to home screen",
|
"Not now, return to home screen": "Not now, return to home screen",
|
||||||
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
"Not registered yet? <2>Create an account</2>": "Not registered yet? <2>Create an account</2>",
|
||||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>",
|
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Passwords must match": "Passwords must match",
|
"Passwords must match": "Passwords must match",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
@@ -91,7 +86,9 @@
|
|||||||
"Sending debug logs…": "Sending debug logs…",
|
"Sending debug logs…": "Sending debug logs…",
|
||||||
"Sending…": "Sending…",
|
"Sending…": "Sending…",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
|
"Share": "Share",
|
||||||
"Share screen": "Share screen",
|
"Share screen": "Share screen",
|
||||||
|
"Share this call": "Share this call",
|
||||||
"Sharing screen": "Sharing screen",
|
"Sharing screen": "Sharing screen",
|
||||||
"Show call inspector": "Show call inspector",
|
"Show call inspector": "Show call inspector",
|
||||||
"Show connection stats": "Show connection stats",
|
"Show connection stats": "Show connection stats",
|
||||||
@@ -106,7 +103,6 @@
|
|||||||
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
|
"Thanks, we received your feedback!": "Thanks, we received your feedback!",
|
||||||
"Thanks!": "Thanks!",
|
"Thanks!": "Thanks!",
|
||||||
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
|
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
|
||||||
"This call is not end-to-end encrypted.": "This call is not end-to-end encrypted.",
|
|
||||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
|
||||||
"User menu": "User menu",
|
"User menu": "User menu",
|
||||||
"Username": "Username",
|
"Username": "Username",
|
||||||
@@ -119,8 +115,8 @@
|
|||||||
"Waiting for other participants…": "Waiting for other participants…",
|
"Waiting for other participants…": "Waiting for other participants…",
|
||||||
"Walkie-talkie call": "Walkie-talkie call",
|
"Walkie-talkie call": "Walkie-talkie call",
|
||||||
"Walkie-talkie call name": "Walkie-talkie call name",
|
"Walkie-talkie call name": "Walkie-talkie call name",
|
||||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.",
|
|
||||||
"Yes, join call": "Yes, join call",
|
"Yes, join call": "Yes, join call",
|
||||||
|
"You": "You",
|
||||||
"You were disconnected from the call": "You were disconnected from the call",
|
"You were disconnected from the call": "You were disconnected from the call",
|
||||||
"Your feedback": "Your feedback",
|
"Your feedback": "Your feedback",
|
||||||
"Your recent calls": "Your recent calls"
|
"Your recent calls": "Your recent calls"
|
||||||
|
|||||||
@@ -16,12 +16,8 @@
|
|||||||
"Version: {{version}}": "Versión: {{version}}",
|
"Version: {{version}}": "Versión: {{version}}",
|
||||||
"Username": "Nombre de usuario",
|
"Username": "Nombre de usuario",
|
||||||
"User menu": "Menú de usuario",
|
"User menu": "Menú de usuario",
|
||||||
"Unmute microphone": "Desilenciar el micrófono",
|
|
||||||
"Turn on camera": "Encender la cámara",
|
|
||||||
"Turn off camera": "Apagar la cámara",
|
|
||||||
"Take me Home": "Volver al inicio",
|
"Take me Home": "Volver al inicio",
|
||||||
"Submit feedback": "Enviar comentarios",
|
"Submit feedback": "Enviar comentarios",
|
||||||
"Stop sharing screen": "Dejar de compartir pantalla",
|
|
||||||
"Spotlight": "Foco",
|
"Spotlight": "Foco",
|
||||||
"Speaker": "Altavoz",
|
"Speaker": "Altavoz",
|
||||||
"Sign out": "Cerrar sesión",
|
"Sign out": "Cerrar sesión",
|
||||||
@@ -44,34 +40,26 @@
|
|||||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Otros usuarios están intentando unirse a la llamada con versiones incompatibles. Estos usuarios deberán asegurarse de que han refrescado sus navegadores:<1>{userLis}</1>",
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Otros usuarios están intentando unirse a la llamada con versiones incompatibles. Estos usuarios deberán asegurarse de que han refrescado sus navegadores:<1>{userLis}</1>",
|
||||||
"Not now, return to home screen": "Ahora no, volver a la pantalla de inicio",
|
"Not now, return to home screen": "Ahora no, volver a la pantalla de inicio",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Mute microphone": "Silenciar micrófono",
|
|
||||||
"More": "Más",
|
"More": "Más",
|
||||||
"Microphone": "Micrófono",
|
"Microphone": "Micrófono",
|
||||||
"Login": "Iniciar sesión",
|
"Login": "Iniciar sesión",
|
||||||
"Logging in…": "Iniciando sesión…",
|
"Logging in…": "Iniciando sesión…",
|
||||||
"Local volume": "Volumen local",
|
"Local volume": "Volumen local",
|
||||||
"Loading…": "Cargando…",
|
"Loading…": "Cargando…",
|
||||||
"Leave": "Abandonar",
|
|
||||||
"Join existing call?": "¿Unirse a llamada existente?",
|
"Join existing call?": "¿Unirse a llamada existente?",
|
||||||
"Join call now": "Unirse a la llamada ahora",
|
"Join call now": "Unirse a la llamada ahora",
|
||||||
"Join call": "Unirse a la llamada",
|
"Join call": "Unirse a la llamada",
|
||||||
"Invite people": "Invitar a gente",
|
|
||||||
"Invite": "Invitar",
|
|
||||||
"Inspector": "Inspector",
|
"Inspector": "Inspector",
|
||||||
"Incompatible versions!": "¡Versiones incompatibles!",
|
|
||||||
"Incompatible versions": "Versiones incompatibles",
|
"Incompatible versions": "Versiones incompatibles",
|
||||||
"Include debug logs": "Incluir registros de depuración",
|
"Include debug logs": "Incluir registros de depuración",
|
||||||
"Home": "Inicio",
|
"Home": "Inicio",
|
||||||
"Grid layout menu": "Menú de distribución de cuadrícula",
|
|
||||||
"Go": "Comenzar",
|
"Go": "Comenzar",
|
||||||
"Full screen": "Pantalla completa",
|
"Full screen": "Pantalla completa",
|
||||||
"Freedom": "Libre",
|
|
||||||
"Fetching group call timed out.": "Se ha agotado el tiempo de espera para obtener la llamada grupal.",
|
"Fetching group call timed out.": "Se ha agotado el tiempo de espera para obtener la llamada grupal.",
|
||||||
"Exit full screen": "Salir de pantalla completa",
|
"Exit full screen": "Salir de pantalla completa",
|
||||||
"Download debug logs": "Descargar registros de depuración",
|
"Download debug logs": "Descargar registros de depuración",
|
||||||
"Display name": "Nombre a mostrar",
|
"Display name": "Nombre a mostrar",
|
||||||
"Developer": "Desarrollador",
|
"Developer": "Desarrollador",
|
||||||
"Details": "Detalles",
|
|
||||||
"Debug log request": "Petición de registros de depuración",
|
"Debug log request": "Petición de registros de depuración",
|
||||||
"Debug log": "Registro de depuración",
|
"Debug log": "Registro de depuración",
|
||||||
"Create account": "Crear cuenta",
|
"Create account": "Crear cuenta",
|
||||||
@@ -79,12 +67,10 @@
|
|||||||
"Copied!": "¡Copiado!",
|
"Copied!": "¡Copiado!",
|
||||||
"Confirm password": "Confirmar contraseña",
|
"Confirm password": "Confirmar contraseña",
|
||||||
"Close": "Cerrar",
|
"Close": "Cerrar",
|
||||||
"Change layout": "Cambiar distribución",
|
|
||||||
"Camera": "Cámara",
|
"Camera": "Cámara",
|
||||||
"Call type menu": "Menú de tipo de llamada",
|
"Call type menu": "Menú de tipo de llamada",
|
||||||
"Call link copied": "Enlace de la llamada copiado",
|
"Call link copied": "Enlace de la llamada copiado",
|
||||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Otro usuario en esta llamada está teniendo problemas. Para diagnosticar estos problemas nos gustaría recopilar un registro de depuración.",
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Otro usuario en esta llamada está teniendo problemas. Para diagnosticar estos problemas nos gustaría recopilar un registro de depuración.",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Audio": "Audio",
|
"Audio": "Audio",
|
||||||
"Avatar": "Avatar",
|
"Avatar": "Avatar",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Crear una cuenta</0> o <2>Acceder como invitado</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Crear una cuenta</0> o <2>Acceder como invitado</2>",
|
||||||
|
|||||||
@@ -3,24 +3,17 @@
|
|||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Liitu kõnega kohe</0><1> Või</1><2>Kopeeri kõne link ja liitu hiljem</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Liitu kõnega kohe</0><1> Või</1><2>Kopeeri kõne link ja liitu hiljem</2>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Loo konto</0> Või <2>Sisene külalisena</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Loo konto</0> Või <2>Sisene külalisena</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>On sul juba konto?</0><1><0>Logi sisse</0> Või <2>Logi sisse külalisena</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>On sul juba konto?</0><1><0>Logi sisse</0> Või <2>Logi sisse külalisena</2></1>",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Invite people": "Kutsu inimesi",
|
|
||||||
"Invite": "Kutsu",
|
|
||||||
"Inspector": "Inspektor",
|
"Inspector": "Inspektor",
|
||||||
"Incompatible versions!": "Ühildumatud versioonid!",
|
|
||||||
"Incompatible versions": "Ühildumatud versioonid",
|
"Incompatible versions": "Ühildumatud versioonid",
|
||||||
"Include debug logs": "Lisa veatuvastuslogid",
|
"Include debug logs": "Lisa veatuvastuslogid",
|
||||||
"Home": "Avavaatesse",
|
"Home": "Avavaatesse",
|
||||||
"Grid layout menu": "Ruudustikvaate menüü",
|
|
||||||
"Go": "Jätka",
|
"Go": "Jätka",
|
||||||
"Full screen": "Täisekraan",
|
"Full screen": "Täisekraan",
|
||||||
"Freedom": "Vaba",
|
|
||||||
"Fetching group call timed out.": "Grupikõne kättesaamine aegus.",
|
"Fetching group call timed out.": "Grupikõne kättesaamine aegus.",
|
||||||
"Exit full screen": "Välju täisekraanivaatest",
|
"Exit full screen": "Välju täisekraanivaatest",
|
||||||
"Download debug logs": "Lae alla veatuvastuslogid",
|
"Download debug logs": "Lae alla veatuvastuslogid",
|
||||||
"Display name": "Kuvatav nimi",
|
"Display name": "Kuvatav nimi",
|
||||||
"Developer": "Arendaja",
|
"Developer": "Arendaja",
|
||||||
"Details": "Täpsemalt",
|
|
||||||
"Debug log request": "Veaotsingulogi päring",
|
"Debug log request": "Veaotsingulogi päring",
|
||||||
"Debug log": "Veaotsingulogi",
|
"Debug log": "Veaotsingulogi",
|
||||||
"Create account": "Loo konto",
|
"Create account": "Loo konto",
|
||||||
@@ -28,7 +21,6 @@
|
|||||||
"Copied!": "Kopeeritud!",
|
"Copied!": "Kopeeritud!",
|
||||||
"Confirm password": "Kinnita salasõna",
|
"Confirm password": "Kinnita salasõna",
|
||||||
"Close": "Sulge",
|
"Close": "Sulge",
|
||||||
"Change layout": "Muuda paigutust",
|
|
||||||
"Camera": "Kaamera",
|
"Camera": "Kaamera",
|
||||||
"Call type menu": "Kõnetüübi valik",
|
"Call type menu": "Kõnetüübi valik",
|
||||||
"Call link copied": "Kõne link on kopeeritud",
|
"Call link copied": "Kõne link on kopeeritud",
|
||||||
@@ -40,7 +32,6 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "Sa pole veel registreerunud? <2>Loo kasutajakonto</2>",
|
"Not registered yet? <2>Create an account</2>": "Sa pole veel registreerunud? <2>Loo kasutajakonto</2>",
|
||||||
"Not now, return to home screen": "Mitte praegu, mine tagasi avalehele",
|
"Not now, return to home screen": "Mitte praegu, mine tagasi avalehele",
|
||||||
"No": "Ei",
|
"No": "Ei",
|
||||||
"Mute microphone": "Summuta mikrofon",
|
|
||||||
"Your recent calls": "Hiljutised kõned",
|
"Your recent calls": "Hiljutised kõned",
|
||||||
"More": "Rohkem",
|
"More": "Rohkem",
|
||||||
"Microphone": "Mikrofon",
|
"Microphone": "Mikrofon",
|
||||||
@@ -49,15 +40,11 @@
|
|||||||
"Logging in…": "Sisselogimine …",
|
"Logging in…": "Sisselogimine …",
|
||||||
"Local volume": "Kohalik helitugevus",
|
"Local volume": "Kohalik helitugevus",
|
||||||
"Loading…": "Laadimine …",
|
"Loading…": "Laadimine …",
|
||||||
"Leave": "Lahku",
|
|
||||||
"Join existing call?": "Liitu juba käimasoleva kõnega?",
|
"Join existing call?": "Liitu juba käimasoleva kõnega?",
|
||||||
"Join call now": "Liitu kõnega kohe",
|
"Join call now": "Liitu kõnega kohe",
|
||||||
"Join call": "Kõnega liitumine",
|
"Join call": "Kõnega liitumine",
|
||||||
"Turn on camera": "Lülita kaamera sisse",
|
|
||||||
"Turn off camera": "Lülita kaamera välja",
|
|
||||||
"Take me Home": "Mine avalehele",
|
"Take me Home": "Mine avalehele",
|
||||||
"Submit feedback": "Jaga tagasisidet",
|
"Submit feedback": "Jaga tagasisidet",
|
||||||
"Stop sharing screen": "Lõpeta ekraani jagamine",
|
|
||||||
"Spotlight": "Rambivalgus",
|
"Spotlight": "Rambivalgus",
|
||||||
"Speaker": "Kõlar",
|
"Speaker": "Kõlar",
|
||||||
"Sign out": "Logi välja",
|
"Sign out": "Logi välja",
|
||||||
@@ -84,7 +71,6 @@
|
|||||||
"Version: {{version}}": "Versioon: {{version}}",
|
"Version: {{version}}": "Versioon: {{version}}",
|
||||||
"Username": "Kasutajanimi",
|
"Username": "Kasutajanimi",
|
||||||
"This call already exists, would you like to join?": "See kõne on juba olemas, kas soovid liituda?",
|
"This call already exists, would you like to join?": "See kõne on juba olemas, kas soovid liituda?",
|
||||||
"Unmute microphone": "Aktiveeri mikrofon",
|
|
||||||
"User menu": "Kasutajamenüü",
|
"User menu": "Kasutajamenüü",
|
||||||
"Yes, join call": "Jah, liitu kõnega",
|
"Yes, join call": "Jah, liitu kõnega",
|
||||||
"Walkie-talkie call": "Walkie-talkie stiilis kõne",
|
"Walkie-talkie call": "Walkie-talkie stiilis kõne",
|
||||||
@@ -122,6 +108,5 @@
|
|||||||
"Reconnect": "Ühenda uuesti",
|
"Reconnect": "Ühenda uuesti",
|
||||||
"Thanks!": "Tänud!",
|
"Thanks!": "Tänud!",
|
||||||
"End-to-end encryption isn't supported on your browser.": "Sinu brauser ei toeta läbivat krüptimist.",
|
"End-to-end encryption isn't supported on your browser.": "Sinu brauser ei toeta läbivat krüptimist.",
|
||||||
"Enable end-to-end encryption (password protected calls)": "Võta kasutusele läbiv krüptimine (salasõnaga kaitstud kõned)",
|
"Enable end-to-end encryption (password protected calls)": "Võta kasutusele läbiv krüptimine (salasõnaga kaitstud kõned)"
|
||||||
"Password (if none, E2EE is disabled)": "Salasõna (tühja väärtuse puhul läbivat krüptimist ei kasutata)"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
"Video call": "تماس تصویری",
|
"Video call": "تماس تصویری",
|
||||||
"Video": "ویدیو",
|
"Video": "ویدیو",
|
||||||
"Username": "نام کاربری",
|
"Username": "نام کاربری",
|
||||||
"Turn on camera": "روشن کردن دوربین",
|
|
||||||
"Turn off camera": "خاموش کردن دوربین",
|
|
||||||
"Take me Home": "مرا به خانه ببر",
|
"Take me Home": "مرا به خانه ببر",
|
||||||
"Speaker": "بلندگو",
|
"Speaker": "بلندگو",
|
||||||
"Sign out": "خروج",
|
"Sign out": "خروج",
|
||||||
@@ -13,27 +11,21 @@
|
|||||||
"Profile": "پروفایل",
|
"Profile": "پروفایل",
|
||||||
"Password": "رمز عبور",
|
"Password": "رمز عبور",
|
||||||
"No": "خیر",
|
"No": "خیر",
|
||||||
"Mute microphone": "بیصدا کردن میکروفون",
|
|
||||||
"More": "بیشتر",
|
"More": "بیشتر",
|
||||||
"Microphone": "میکروفون",
|
"Microphone": "میکروفون",
|
||||||
"Login to your account": "به حساب کاربری خود وارد شوید",
|
"Login to your account": "به حساب کاربری خود وارد شوید",
|
||||||
"Login": "ورود",
|
"Login": "ورود",
|
||||||
"Loading…": "بارگزاری…",
|
"Loading…": "بارگزاری…",
|
||||||
"Leave": "خروج",
|
|
||||||
"Join existing call?": "پیوست به تماس؟",
|
"Join existing call?": "پیوست به تماس؟",
|
||||||
"Join call now": "الان به تماس بپیوند",
|
"Join call now": "الان به تماس بپیوند",
|
||||||
"Join call": "پیوستن به تماس",
|
"Join call": "پیوستن به تماس",
|
||||||
"Invite people": "دعوت از افراد",
|
|
||||||
"Invite": "دعوت",
|
|
||||||
"Home": "خانه",
|
"Home": "خانه",
|
||||||
"Go": "رفتن",
|
"Go": "رفتن",
|
||||||
"Full screen": "تمام صحفه",
|
"Full screen": "تمام صحفه",
|
||||||
"Freedom": "آزادی",
|
|
||||||
"Exit full screen": "خروج از حالت تمام صفحه",
|
"Exit full screen": "خروج از حالت تمام صفحه",
|
||||||
"Download debug logs": "دانلود لاگ عیبیابی",
|
"Download debug logs": "دانلود لاگ عیبیابی",
|
||||||
"Display name": "نام نمایشی",
|
"Display name": "نام نمایشی",
|
||||||
"Developer": "توسعه دهنده",
|
"Developer": "توسعه دهنده",
|
||||||
"Details": "جزئیات",
|
|
||||||
"Debug log request": "درخواست لاگ عیبیابی",
|
"Debug log request": "درخواست لاگ عیبیابی",
|
||||||
"Debug log": "لاگ عیبیابی",
|
"Debug log": "لاگ عیبیابی",
|
||||||
"Create account": "ساخت حساب کاربری",
|
"Create account": "ساخت حساب کاربری",
|
||||||
@@ -41,20 +33,17 @@
|
|||||||
"Copied!": "کپی شد!",
|
"Copied!": "کپی شد!",
|
||||||
"Confirm password": "تایید رمزعبور",
|
"Confirm password": "تایید رمزعبور",
|
||||||
"Close": "بستن",
|
"Close": "بستن",
|
||||||
"Change layout": "تغییر طرح",
|
|
||||||
"Camera": "دوربین",
|
"Camera": "دوربین",
|
||||||
"Call type menu": "منوی نوع تماس",
|
"Call type menu": "منوی نوع تماس",
|
||||||
"Call link copied": "لینک تماس کپی شد",
|
"Call link copied": "لینک تماس کپی شد",
|
||||||
"Avatar": "آواتار",
|
"Avatar": "آواتار",
|
||||||
"Audio": "صدا",
|
"Audio": "صدا",
|
||||||
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیبیابی را جمعآوری کنیم.",
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "کاربر دیگری در این تماس مشکلی دارد. برای تشخیص بهتر مشکل، بهتر است ما لاگ عیبیابی را جمعآوری کنیم.",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>چرا یک رمز عبور برای حساب کاربری خود تنظیم نمیکنید؟</0><1>شما میتوانید نام خود را حفظ کنید و یک آواتار برای تماسهای آینده بسازید</1>",
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>چرا یک رمز عبور برای حساب کاربری خود تنظیم نمیکنید؟</0><1>شما میتوانید نام خود را حفظ کنید و یک آواتار برای تماسهای آینده بسازید</1>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>ساخت حساب کاربری</0> Or <2>دسترسی به عنوان میهمان</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>ساخت حساب کاربری</0> Or <2>دسترسی به عنوان میهمان</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>از قبل حساب کاربری دارید؟</0><1><0>ورود</0> Or <2>به عنوان یک میهمان وارد شوید</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>از قبل حساب کاربری دارید؟</0><1><0>ورود</0> Or <2>به عنوان یک میهمان وارد شوید</2></1>",
|
||||||
"Local volume": "حجم داخلی",
|
"Local volume": "حجم داخلی",
|
||||||
"Inspector": "بازرس",
|
"Inspector": "بازرس",
|
||||||
"Incompatible versions!": "نسخههای ناسازگار!",
|
|
||||||
"Incompatible versions": "نسخههای ناسازگار",
|
"Incompatible versions": "نسخههای ناسازگار",
|
||||||
"Spotlight": "نور افکن",
|
"Spotlight": "نور افکن",
|
||||||
"Show call inspector": "نمایش بازرس تماس",
|
"Show call inspector": "نمایش بازرس تماس",
|
||||||
@@ -75,7 +64,6 @@
|
|||||||
"Not now, return to home screen": "الان نه، به صفحه اصلی برگردید",
|
"Not now, return to home screen": "الان نه، به صفحه اصلی برگردید",
|
||||||
"Logging in…": "ورود…",
|
"Logging in…": "ورود…",
|
||||||
"Include debug logs": "شامل لاگهای عیبیابی",
|
"Include debug logs": "شامل لاگهای عیبیابی",
|
||||||
"Grid layout menu": "منوی طرحبندی شبکهای",
|
|
||||||
"Fetching group call timed out.": "زمان اتصال به مکالمه گروهی تمام شد.",
|
"Fetching group call timed out.": "زمان اتصال به مکالمه گروهی تمام شد.",
|
||||||
"Yes, join call": "بله، به تماس بپیوندید",
|
"Yes, join call": "بله، به تماس بپیوندید",
|
||||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC (ارتباطات رسانهای بلادرنگ مانند انتقال صدا، ویدئو و داده) در این مرورگر پشتیبانی نمیشود یا در حال مسدود شدن است.",
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC (ارتباطات رسانهای بلادرنگ مانند انتقال صدا، ویدئو و داده) در این مرورگر پشتیبانی نمیشود یا در حال مسدود شدن است.",
|
||||||
@@ -85,10 +73,8 @@
|
|||||||
"Video call name": "نامِ تماسِ تصویری",
|
"Video call name": "نامِ تماسِ تصویری",
|
||||||
"Version: {{version}}": "نسخه: {{نسخه}}",
|
"Version: {{version}}": "نسخه: {{نسخه}}",
|
||||||
"User menu": "فهرست کاربر",
|
"User menu": "فهرست کاربر",
|
||||||
"Unmute microphone": "ناخموشی میکروفون",
|
|
||||||
"This call already exists, would you like to join?": "این تماس از قبل وجود دارد، میخواهید بپیوندید؟",
|
"This call already exists, would you like to join?": "این تماس از قبل وجود دارد، میخواهید بپیوندید؟",
|
||||||
"Submit feedback": "بازخورد ارائه دهید",
|
"Submit feedback": "بازخورد ارائه دهید",
|
||||||
"Stop sharing screen": "توقف اشتراکگذاری صفحه نمایش",
|
|
||||||
"Element Call Home": "خانهٔ تماس المنت",
|
"Element Call Home": "خانهٔ تماس المنت",
|
||||||
"Copy": "رونوشت",
|
"Copy": "رونوشت",
|
||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>اکنون به تماس پیوسته</0><1>یا</1><2>پیوند تماس را رونوشت کرده و بعداً بپیوندید</2>"
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>اکنون به تماس پیوسته</0><1>یا</1><2>پیوند تماس را رونوشت کرده و بعداً بپیوندید</2>"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"Call link copied": "Lien de l’appel copié",
|
"Call link copied": "Lien de l’appel copié",
|
||||||
"Call type menu": "Menu de type d’appel",
|
"Call type menu": "Menu de type d’appel",
|
||||||
"Camera": "Caméra",
|
"Camera": "Caméra",
|
||||||
"Change layout": "Changer la disposition",
|
|
||||||
"Close": "Fermer",
|
"Close": "Fermer",
|
||||||
"Confirm password": "Confirmer le mot de passe",
|
"Confirm password": "Confirmer le mot de passe",
|
||||||
"Copied!": "Copié !",
|
"Copied!": "Copié !",
|
||||||
@@ -15,25 +14,19 @@
|
|||||||
"Create account": "Créer un compte",
|
"Create account": "Créer un compte",
|
||||||
"Debug log": "Journal de débogage",
|
"Debug log": "Journal de débogage",
|
||||||
"Debug log request": "Demande d’un journal de débogage",
|
"Debug log request": "Demande d’un journal de débogage",
|
||||||
"Details": "Informations",
|
|
||||||
"Developer": "Développeur",
|
"Developer": "Développeur",
|
||||||
"Display name": "Nom d’affichage",
|
"Display name": "Nom d’affichage",
|
||||||
"Download debug logs": "Télécharger les journaux de débogage",
|
"Download debug logs": "Télécharger les journaux de débogage",
|
||||||
"Exit full screen": "Quitter le plein écran",
|
"Exit full screen": "Quitter le plein écran",
|
||||||
"Freedom": "Libre",
|
|
||||||
"Full screen": "Plein écran",
|
"Full screen": "Plein écran",
|
||||||
"Go": "Commencer",
|
"Go": "Commencer",
|
||||||
"Grid layout menu": "Menu en grille",
|
|
||||||
"Home": "Accueil",
|
"Home": "Accueil",
|
||||||
"Include debug logs": "Inclure les journaux de débogage",
|
"Include debug logs": "Inclure les journaux de débogage",
|
||||||
"Incompatible versions": "Versions incompatibles",
|
"Incompatible versions": "Versions incompatibles",
|
||||||
"Incompatible versions!": "Versions incompatibles !",
|
|
||||||
"Inspector": "Inspecteur",
|
"Inspector": "Inspecteur",
|
||||||
"Invite people": "Inviter des gens",
|
|
||||||
"Join call": "Rejoindre l’appel",
|
"Join call": "Rejoindre l’appel",
|
||||||
"Join call now": "Rejoindre l’appel maintenant",
|
"Join call now": "Rejoindre l’appel maintenant",
|
||||||
"Join existing call?": "Rejoindre un appel existant ?",
|
"Join existing call?": "Rejoindre un appel existant ?",
|
||||||
"Leave": "Partir",
|
|
||||||
"Loading…": "Chargement…",
|
"Loading…": "Chargement…",
|
||||||
"Local volume": "Volume local",
|
"Local volume": "Volume local",
|
||||||
"Logging in…": "Connexion…",
|
"Logging in…": "Connexion…",
|
||||||
@@ -41,7 +34,6 @@
|
|||||||
"Login to your account": "Connectez vous à votre compte",
|
"Login to your account": "Connectez vous à votre compte",
|
||||||
"Microphone": "Microphone",
|
"Microphone": "Microphone",
|
||||||
"More": "Plus",
|
"More": "Plus",
|
||||||
"Mute microphone": "Couper le micro",
|
|
||||||
"No": "Non",
|
"No": "Non",
|
||||||
"Not now, return to home screen": "Pas maintenant, retourner à l’accueil",
|
"Not now, return to home screen": "Pas maintenant, retourner à l’accueil",
|
||||||
"Not registered yet? <2>Create an account</2>": "Pas encore de compte ? <2>En créer un</2>",
|
"Not registered yet? <2>Create an account</2>": "Pas encore de compte ? <2>En créer un</2>",
|
||||||
@@ -64,12 +56,10 @@
|
|||||||
"Sign in": "Connexion",
|
"Sign in": "Connexion",
|
||||||
"Sign out": "Déconnexion",
|
"Sign out": "Déconnexion",
|
||||||
"Spotlight": "Premier plan",
|
"Spotlight": "Premier plan",
|
||||||
"Stop sharing screen": "Arrêter le partage d’écran",
|
|
||||||
"Submit feedback": "Envoyer des retours",
|
"Submit feedback": "Envoyer des retours",
|
||||||
"Take me Home": "Retouner à l’accueil",
|
"Take me Home": "Retouner à l’accueil",
|
||||||
"This call already exists, would you like to join?": "Cet appel existe déjà, voulez-vous le rejoindre ?",
|
"This call already exists, would you like to join?": "Cet appel existe déjà, voulez-vous le rejoindre ?",
|
||||||
"Fetching group call timed out.": "Échec de connexion à l’appel de groupe dans le temps imparti.",
|
"Fetching group call timed out.": "Échec de connexion à l’appel de groupe dans le temps imparti.",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Your recent calls": "Appels récents",
|
"Your recent calls": "Appels récents",
|
||||||
"Yes, join call": "Oui, rejoindre l’appel",
|
"Yes, join call": "Oui, rejoindre l’appel",
|
||||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC n’est pas pris en charge ou est bloqué par ce navigateur.",
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC n’est pas pris en charge ou est bloqué par ce navigateur.",
|
||||||
@@ -82,11 +72,7 @@
|
|||||||
"Version: {{version}}": "Version : {{version}}",
|
"Version: {{version}}": "Version : {{version}}",
|
||||||
"Username": "Nom d’utilisateur",
|
"Username": "Nom d’utilisateur",
|
||||||
"User menu": "Menu utilisateur",
|
"User menu": "Menu utilisateur",
|
||||||
"Unmute microphone": "Allumer le micro",
|
|
||||||
"Turn on camera": "Allumer la caméra",
|
|
||||||
"Turn off camera": "Couper la caméra",
|
|
||||||
"Speaker": "Intervenant",
|
"Speaker": "Intervenant",
|
||||||
"Invite": "Inviter",
|
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Vous avez déjà un compte ?</0><1><0>Se connecter</0> Ou <2>Accès invité</2></1>",
|
||||||
"Sending debug logs…": "Envoi des journaux de débogage…",
|
"Sending debug logs…": "Envoi des journaux de débogage…",
|
||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Rejoindre l’appel maintenant</0><1>Ou</1><2>Copier le lien de l’appel et rejoindre plus tard</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Rejoindre l’appel maintenant</0><1>Ou</1><2>Copier le lien de l’appel et rejoindre plus tard</2>",
|
||||||
@@ -122,6 +108,5 @@
|
|||||||
"You were disconnected from the call": "Vous avez été déconnecté de l’appel",
|
"You were disconnected from the call": "Vous avez été déconnecté de l’appel",
|
||||||
"Connectivity to the server has been lost.": "La connexion avec le serveur a été perdue.",
|
"Connectivity to the server has been lost.": "La connexion avec le serveur a été perdue.",
|
||||||
"End-to-end encryption isn't supported on your browser.": "Le chiffrement de bout-en-bout n’est pas pris en charge par votre navigateur.",
|
"End-to-end encryption isn't supported on your browser.": "Le chiffrement de bout-en-bout n’est pas pris en charge par votre navigateur.",
|
||||||
"Password (if none, E2EE is disabled)": "Mot de passe (si aucun, le chiffrement E2EE est désactivé)",
|
|
||||||
"Enable end-to-end encryption (password protected calls)": "Activer le chiffrement de bout-en-bout (appels protégés par mot de passe)"
|
"Enable end-to-end encryption (password protected calls)": "Activer le chiffrement de bout-en-bout (appels protégés par mot de passe)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"Call link copied": "Tautan panggilan disalin",
|
"Call link copied": "Tautan panggilan disalin",
|
||||||
"Call type menu": "Menu jenis panggilan",
|
"Call type menu": "Menu jenis panggilan",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
"Change layout": "Ubah tata letak",
|
|
||||||
"Close": "Tutup",
|
"Close": "Tutup",
|
||||||
"Confirm password": "Konfirmasi kata sandi",
|
"Confirm password": "Konfirmasi kata sandi",
|
||||||
"Copied!": "Disalin!",
|
"Copied!": "Disalin!",
|
||||||
@@ -16,27 +15,20 @@
|
|||||||
"Create account": "Buat akun",
|
"Create account": "Buat akun",
|
||||||
"Debug log": "Catatan pengawakutuan",
|
"Debug log": "Catatan pengawakutuan",
|
||||||
"Debug log request": "Permintaan catatan pengawakutuan",
|
"Debug log request": "Permintaan catatan pengawakutuan",
|
||||||
"Details": "Detail",
|
|
||||||
"Developer": "Pengembang",
|
"Developer": "Pengembang",
|
||||||
"Display name": "Nama tampilan",
|
"Display name": "Nama tampilan",
|
||||||
"Download debug logs": "Unduh catatan pengawakutuan",
|
"Download debug logs": "Unduh catatan pengawakutuan",
|
||||||
"Exit full screen": "Keluar dari layar penuh",
|
"Exit full screen": "Keluar dari layar penuh",
|
||||||
"Fetching group call timed out.": "Waktu pendapatan panggilan grup habis.",
|
"Fetching group call timed out.": "Waktu pendapatan panggilan grup habis.",
|
||||||
"Freedom": "Bebas",
|
|
||||||
"Full screen": "Layar penuh",
|
"Full screen": "Layar penuh",
|
||||||
"Go": "Bergabung",
|
"Go": "Bergabung",
|
||||||
"Grid layout menu": "Menu tata letak kisi",
|
|
||||||
"Home": "Beranda",
|
"Home": "Beranda",
|
||||||
"Include debug logs": "Termasuk catatan pengawakutuan",
|
"Include debug logs": "Termasuk catatan pengawakutuan",
|
||||||
"Incompatible versions": "Versi tidak kompatibel",
|
"Incompatible versions": "Versi tidak kompatibel",
|
||||||
"Incompatible versions!": "Versi tidak kompatibel!",
|
|
||||||
"Inspector": "Inspektur",
|
"Inspector": "Inspektur",
|
||||||
"Invite": "Undang",
|
|
||||||
"Invite people": "Undang orang",
|
|
||||||
"Join call": "Bergabung ke panggilan",
|
"Join call": "Bergabung ke panggilan",
|
||||||
"Join call now": "Bergabung ke panggilan sekarang",
|
"Join call now": "Bergabung ke panggilan sekarang",
|
||||||
"Join existing call?": "Bergabung ke panggilan yang sudah ada?",
|
"Join existing call?": "Bergabung ke panggilan yang sudah ada?",
|
||||||
"Leave": "Keluar",
|
|
||||||
"Loading…": "Memuat…",
|
"Loading…": "Memuat…",
|
||||||
"Local volume": "Volume lokal",
|
"Local volume": "Volume lokal",
|
||||||
"Logging in…": "Memasuki…",
|
"Logging in…": "Memasuki…",
|
||||||
@@ -44,7 +36,6 @@
|
|||||||
"Login to your account": "Masuk ke akun Anda",
|
"Login to your account": "Masuk ke akun Anda",
|
||||||
"Microphone": "Mikrofon",
|
"Microphone": "Mikrofon",
|
||||||
"More": "Lainnya",
|
"More": "Lainnya",
|
||||||
"Mute microphone": "Bisukan mikrofon",
|
|
||||||
"No": "Tidak",
|
"No": "Tidak",
|
||||||
"Not now, return to home screen": "Tidak sekarang, kembali ke layar beranda",
|
"Not now, return to home screen": "Tidak sekarang, kembali ke layar beranda",
|
||||||
"Not registered yet? <2>Create an account</2>": "Belum terdaftar? <2>Buat sebuah akun</2>",
|
"Not registered yet? <2>Create an account</2>": "Belum terdaftar? <2>Buat sebuah akun</2>",
|
||||||
@@ -68,13 +59,9 @@
|
|||||||
"Sign out": "Keluar",
|
"Sign out": "Keluar",
|
||||||
"Speaker": "Pembicara",
|
"Speaker": "Pembicara",
|
||||||
"Spotlight": "Sorotan",
|
"Spotlight": "Sorotan",
|
||||||
"Stop sharing screen": "Berhenti membagikan layar",
|
|
||||||
"Submit feedback": "Kirim masukan",
|
"Submit feedback": "Kirim masukan",
|
||||||
"Take me Home": "Bawa saya ke Beranda",
|
"Take me Home": "Bawa saya ke Beranda",
|
||||||
"This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?",
|
"This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?",
|
||||||
"Turn off camera": "Matikan kamera",
|
|
||||||
"Turn on camera": "Nyalakan kamera",
|
|
||||||
"Unmute microphone": "Suarakan mikrofon",
|
|
||||||
"User menu": "Menu pengguna",
|
"User menu": "Menu pengguna",
|
||||||
"Username": "Nama pengguna",
|
"Username": "Nama pengguna",
|
||||||
"Version: {{version}}": "Versi: {{version}}",
|
"Version: {{version}}": "Versi: {{version}}",
|
||||||
@@ -87,7 +74,6 @@
|
|||||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC tidak didukung atau diblokir di peramban ini.",
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC tidak didukung atau diblokir di peramban ini.",
|
||||||
"Yes, join call": "Ya, bergabung ke panggilan",
|
"Yes, join call": "Ya, bergabung ke panggilan",
|
||||||
"Your recent calls": "Panggilan Anda terkini",
|
"Your recent calls": "Panggilan Anda terkini",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Sending debug logs…": "Mengirimkan catatan pengawakutuan…",
|
"Sending debug logs…": "Mengirimkan catatan pengawakutuan…",
|
||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung ke panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Bergabung ke panggilan sekarang</0><1>Atau</1><2>Salin tautan dan bergabung nanti</2>",
|
||||||
"Element Call Home": "Beranda Element Call",
|
"Element Call Home": "Beranda Element Call",
|
||||||
@@ -119,7 +105,6 @@
|
|||||||
"Connectivity to the server has been lost.": "Koneksi ke server telah hilang.",
|
"Connectivity to the server has been lost.": "Koneksi ke server telah hilang.",
|
||||||
"Enable end-to-end encryption (password protected calls)": "Aktifkan enkripsi ujung ke ujung (panggilan terlindungi oleh kata sandi)",
|
"Enable end-to-end encryption (password protected calls)": "Aktifkan enkripsi ujung ke ujung (panggilan terlindungi oleh kata sandi)",
|
||||||
"End-to-end encryption isn't supported on your browser.": "Enkripsi ujung ke ujung tidak didukung di peramban Anda.",
|
"End-to-end encryption isn't supported on your browser.": "Enkripsi ujung ke ujung tidak didukung di peramban Anda.",
|
||||||
"Password (if none, E2EE is disabled)": "Kata sandi (jika tidak ada, enkripsi akan dinonaktifkan)",
|
|
||||||
"Retry sending logs": "Kirim ulang catatan",
|
"Retry sending logs": "Kirim ulang catatan",
|
||||||
"You were disconnected from the call": "Anda terputus dari panggilan",
|
"You were disconnected from the call": "Anda terputus dari panggilan",
|
||||||
"Reconnect": "Hubungkan ulang",
|
"Reconnect": "Hubungkan ulang",
|
||||||
|
|||||||
1
public/locales/it/app.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
"Audio": "音声",
|
"Audio": "音声",
|
||||||
"Confirm password": "パスワードを確認",
|
"Confirm password": "パスワードを確認",
|
||||||
"Close": "閉じる",
|
"Close": "閉じる",
|
||||||
"Change layout": "レイアウトを変更",
|
|
||||||
"Copied!": "コピーしました!",
|
"Copied!": "コピーしました!",
|
||||||
"Copy and share this call link": "通話リンクをコピーし共有",
|
"Copy and share this call link": "通話リンクをコピーし共有",
|
||||||
"Copy": "コピー",
|
"Copy": "コピー",
|
||||||
@@ -21,33 +20,23 @@
|
|||||||
"Download debug logs": "デバッグログをダウンロード",
|
"Download debug logs": "デバッグログをダウンロード",
|
||||||
"Display name": "表示名",
|
"Display name": "表示名",
|
||||||
"Developer": "開発者",
|
"Developer": "開発者",
|
||||||
"Details": "詳細",
|
|
||||||
"Full screen": "全画面表示",
|
"Full screen": "全画面表示",
|
||||||
"Exit full screen": "全画面表示を終了",
|
"Exit full screen": "全画面表示を終了",
|
||||||
"Include debug logs": "デバッグログを含める",
|
"Include debug logs": "デバッグログを含める",
|
||||||
"Home": "ホーム",
|
"Home": "ホーム",
|
||||||
"Incompatible versions!": "互換性のないバージョンです!",
|
|
||||||
"Incompatible versions": "互換性のないバージョン",
|
"Incompatible versions": "互換性のないバージョン",
|
||||||
"Join existing call?": "既存の通話に参加しますか?",
|
"Join existing call?": "既存の通話に参加しますか?",
|
||||||
"Join call now": "今すぐ通話に参加",
|
"Join call now": "今すぐ通話に参加",
|
||||||
"Join call": "通話に参加",
|
"Join call": "通話に参加",
|
||||||
"Invite": "招待",
|
|
||||||
"Invite people": "連絡先を招待",
|
|
||||||
"Not registered yet? <2>Create an account</2>": "アカウントがありませんか? <2>アカウントを作成</2>",
|
"Not registered yet? <2>Create an account</2>": "アカウントがありませんか? <2>アカウントを作成</2>",
|
||||||
"Mute microphone": "マイクをミュート",
|
|
||||||
"Microphone": "マイク",
|
"Microphone": "マイク",
|
||||||
"Login": "ログイン",
|
"Login": "ログイン",
|
||||||
"Logging in…": "ログインしています…",
|
"Logging in…": "ログインしています…",
|
||||||
"Loading…": "読み込んでいます…",
|
"Loading…": "読み込んでいます…",
|
||||||
"Leave": "退出",
|
|
||||||
"Version: {{version}}": "バージョン:{{version}}",
|
"Version: {{version}}": "バージョン:{{version}}",
|
||||||
"Username": "ユーザー名",
|
"Username": "ユーザー名",
|
||||||
"User menu": "ユーザーメニュー",
|
"User menu": "ユーザーメニュー",
|
||||||
"Unmute microphone": "マイクのミュートを解除",
|
|
||||||
"Turn on camera": "カメラをつける",
|
|
||||||
"Turn off camera": "カメラを切る",
|
|
||||||
"Submit feedback": "フィードバックを送信",
|
"Submit feedback": "フィードバックを送信",
|
||||||
"Stop sharing screen": "画面共有を停止",
|
|
||||||
"Spotlight": "スポットライト",
|
"Spotlight": "スポットライト",
|
||||||
"Send debug logs": "デバッグログを送信",
|
"Send debug logs": "デバッグログを送信",
|
||||||
"Sign out": "サインアウト",
|
"Sign out": "サインアウト",
|
||||||
@@ -75,10 +64,8 @@
|
|||||||
"Your recent calls": "最近の通話",
|
"Your recent calls": "最近の通話",
|
||||||
"WebRTC is not supported or is being blocked in this browser.": "お使いのブラウザでWebRTCがサポートされていないか、またはブロックされています。",
|
"WebRTC is not supported or is being blocked in this browser.": "お使いのブラウザでWebRTCがサポートされていないか、またはブロックされています。",
|
||||||
"Login to your account": "アカウントにログイン",
|
"Login to your account": "アカウントにログイン",
|
||||||
"Freedom": "自由",
|
|
||||||
"Remove": "削除",
|
"Remove": "削除",
|
||||||
"No": "いいえ",
|
"No": "いいえ",
|
||||||
"This call already exists, would you like to join?": "この通話は既に存在します。参加しますか?",
|
"This call already exists, would you like to join?": "この通話は既に存在します。参加しますか?",
|
||||||
"Take me Home": "ホームに戻る",
|
"Take me Home": "ホームに戻る"
|
||||||
"{{names}}, {{name}}": "{{names}}、{{name}}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": ""
|
||||||
"{{names}}, {{name}}": "{{names}}님, {{name}}님"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"{{count}} stars|one": "{{count}} zvaigzne",
|
"{{count}} stars|one": "{{count}} zvaigzne",
|
||||||
"{{count}} stars|other": "{{count}} zvaigznes",
|
"{{count}} stars|other": "{{count}} zvaigznes",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Jau ir konts?</0><1><0>Pieteikties</0> vai <2>Piekļūt kā viesim</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Jau ir konts?</0><1><0>Pieteikties</0> vai <2>Piekļūt kā viesim</2></1>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Izveidot kontu</0> vai <2>Piekļūt kā viesim</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Izveidot kontu</0> vai <2>Piekļūt kā viesim</2>",
|
||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Pievienoties zvanam tagad</0><1>vai</1><2>ievietot zvana saiti starpliktuvē un pievienoties vēlāk</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Pievienoties zvanam tagad</0><1>vai</1><2>ievietot zvana saiti starpliktuvē un pievienoties vēlāk</2>",
|
||||||
@@ -15,7 +14,6 @@
|
|||||||
"Call link copied": "Zvana saite ievietota starpliktuvē",
|
"Call link copied": "Zvana saite ievietota starpliktuvē",
|
||||||
"Call type menu": "Zvana veida izvēlne",
|
"Call type menu": "Zvana veida izvēlne",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
"Change layout": "Mainīt izkārtojumu",
|
|
||||||
"Close": "Aizvērt",
|
"Close": "Aizvērt",
|
||||||
"Confirm password": "Apstiprināt paroli",
|
"Confirm password": "Apstiprināt paroli",
|
||||||
"Connectivity to the server has been lost.": "Ir zaudēts savienojums ar serveri.",
|
"Connectivity to the server has been lost.": "Ir zaudēts savienojums ar serveri.",
|
||||||
@@ -25,7 +23,6 @@
|
|||||||
"Create account": "Izveidot kontu",
|
"Create account": "Izveidot kontu",
|
||||||
"Debug log": "Atkļūdošanas žurnāls",
|
"Debug log": "Atkļūdošanas žurnāls",
|
||||||
"Debug log request": "Atkļūdošanas žurnāla pieprasījums",
|
"Debug log request": "Atkļūdošanas žurnāla pieprasījums",
|
||||||
"Details": "Izvērsums",
|
|
||||||
"Developer": "Izstrādātājs",
|
"Developer": "Izstrādātājs",
|
||||||
"Developer Settings": "Izstrādātāja iestatījumi",
|
"Developer Settings": "Izstrādātāja iestatījumi",
|
||||||
"Display name": "Attēlojamais vārds",
|
"Display name": "Attēlojamais vārds",
|
||||||
@@ -35,10 +32,8 @@
|
|||||||
"Expose developer settings in the settings window.": "Izstādīt izstrādātāja iestatījumus iestatījumu logā.",
|
"Expose developer settings in the settings window.": "Izstādīt izstrādātāja iestatījumus iestatījumu logā.",
|
||||||
"Feedback": "Atsauksmes",
|
"Feedback": "Atsauksmes",
|
||||||
"Fetching group call timed out.": "Grupas zvana iegūšanā iestājās noildze.",
|
"Fetching group call timed out.": "Grupas zvana iegūšanā iestājās noildze.",
|
||||||
"Freedom": "Brīvība",
|
|
||||||
"Full screen": "Pilnekrāns",
|
"Full screen": "Pilnekrāns",
|
||||||
"Go": "Aiziet",
|
"Go": "Aiziet",
|
||||||
"Grid layout menu": "Režģa izkārtojuma izvēlne",
|
|
||||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klikšķināšana uz \"Pievienoties zvanam tagad\" apliecina piekrišanu mūsu <2>galalietotāja licencēšanas nolīgumam (GLLN)</2>",
|
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klikšķināšana uz \"Pievienoties zvanam tagad\" apliecina piekrišanu mūsu <2>galalietotāja licencēšanas nolīgumam (GLLN)</2>",
|
||||||
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Piedalīšanās šajā beta apliecina piekrišanu anonīmu datu ievākšanai, ko mēs izmantojam, lai uzlabotu izstrādājumu. Vairāk informācijas par datiem, ko mēs ievācam, var atrast mūsu <2>privātuma nosacījumos</2> un <5>sīkdatņu nosacījumos</5>.",
|
"By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy</2> and our <5>Cookie Policy</5>.": "Piedalīšanās šajā beta apliecina piekrišanu anonīmu datu ievākšanai, ko mēs izmantojam, lai uzlabotu izstrādājumu. Vairāk informācijas par datiem, ko mēs ievācam, var atrast mūsu <2>privātuma nosacījumos</2> un <5>sīkdatņu nosacījumos</5>.",
|
||||||
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call īslaicīgi nav pilnīgi šifrēts, kamēr mēs pārbaudām mērogojamību.",
|
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call īslaicīgi nav pilnīgi šifrēts, kamēr mēs pārbaudām mērogojamību.",
|
||||||
@@ -58,14 +53,10 @@
|
|||||||
"How did it go?": "Kā Tev veicās?",
|
"How did it go?": "Kā Tev veicās?",
|
||||||
"Include debug logs": "Iekļaut atkļūdošanas žurnāla ierakstus",
|
"Include debug logs": "Iekļaut atkļūdošanas žurnāla ierakstus",
|
||||||
"Incompatible versions": "Nesaderīgas versijas",
|
"Incompatible versions": "Nesaderīgas versijas",
|
||||||
"Incompatible versions!": "Nesaderīgas versijas.",
|
|
||||||
"Inspector": "Inspektors",
|
"Inspector": "Inspektors",
|
||||||
"Invite": "Uzaicināt",
|
|
||||||
"Invite people": "Uzaicināt cilvēkus",
|
|
||||||
"Join call": "Pievienoties zvanam",
|
"Join call": "Pievienoties zvanam",
|
||||||
"Join call now": "Pievienoties zvanam tagad",
|
"Join call now": "Pievienoties zvanam tagad",
|
||||||
"Join existing call?": "Pievienoties esošam zvanam?",
|
"Join existing call?": "Pievienoties esošam zvanam?",
|
||||||
"Leave": "Pamest",
|
|
||||||
"Loading…": "Lādējas…",
|
"Loading…": "Lādējas…",
|
||||||
"Local volume": "Vietējais skaļums",
|
"Local volume": "Vietējais skaļums",
|
||||||
"Logging in…": "Piesakās…",
|
"Logging in…": "Piesakās…",
|
||||||
@@ -73,11 +64,9 @@
|
|||||||
"Login to your account": "Pieteikties kontā",
|
"Login to your account": "Pieteikties kontā",
|
||||||
"Microphone": "Mikrofons",
|
"Microphone": "Mikrofons",
|
||||||
"More": "Vairāk",
|
"More": "Vairāk",
|
||||||
"Mute microphone": "Apklusināt mikrofonu",
|
|
||||||
"No": "Nē",
|
"No": "Nē",
|
||||||
"Not now, return to home screen": "Ne tagad, atgriezties sākuma ekrānā",
|
"Not now, return to home screen": "Ne tagad, atgriezties sākuma ekrānā",
|
||||||
"Password": "Parole",
|
"Password": "Parole",
|
||||||
"Password (if none, E2EE is disabled)": "Parole (ja nav, pilnīga šifrēšana ir atspējota)",
|
|
||||||
"Passwords must match": "Parolēm ir jāsakrīt",
|
"Passwords must match": "Parolēm ir jāsakrīt",
|
||||||
"Profile": "Profils",
|
"Profile": "Profils",
|
||||||
"Recaptcha dismissed": "ReCaptcha atmesta",
|
"Recaptcha dismissed": "ReCaptcha atmesta",
|
||||||
@@ -100,7 +89,6 @@
|
|||||||
"Sign out": "Atteikties",
|
"Sign out": "Atteikties",
|
||||||
"Speaker": "Runātājs",
|
"Speaker": "Runātājs",
|
||||||
"Spotlight": "Starmešu gaisma",
|
"Spotlight": "Starmešu gaisma",
|
||||||
"Stop sharing screen": "Pārtraukt ekrāna kopīgošanu",
|
|
||||||
"Submit": "Iesniegt",
|
"Submit": "Iesniegt",
|
||||||
"Submit feedback": "Iesniegt atsauksmi",
|
"Submit feedback": "Iesniegt atsauksmi",
|
||||||
"Submitting…": "Iesniedz…",
|
"Submitting…": "Iesniedz…",
|
||||||
@@ -109,9 +97,6 @@
|
|||||||
"Thanks!": "Paldies!",
|
"Thanks!": "Paldies!",
|
||||||
"This call already exists, would you like to join?": "Šis zvans jau pastāv. Vai vēlies pievienoties?",
|
"This call already exists, would you like to join?": "Šis zvans jau pastāv. Vai vēlies pievienoties?",
|
||||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Šo vietni aizsargā ReCAPTCHA, un ir attiecināmi Google <2>privātuma nosacījumi</2> un <6>pakalpojuma noteikumi</6>.<9></9>Klikšķināšana uz \"Reģistrēties\" sniedz piekrišanu mūsu <12>galalietotāja licencēšanas nolīgumam (GLLN)</12>",
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Šo vietni aizsargā ReCAPTCHA, un ir attiecināmi Google <2>privātuma nosacījumi</2> un <6>pakalpojuma noteikumi</6>.<9></9>Klikšķināšana uz \"Reģistrēties\" sniedz piekrišanu mūsu <12>galalietotāja licencēšanas nolīgumam (GLLN)</12>",
|
||||||
"Turn off camera": "Izslēgt kameru",
|
|
||||||
"Turn on camera": "Ieslēgt kameru",
|
|
||||||
"Unmute microphone": "Atsaukt mikrofona apklusināšanu",
|
|
||||||
"User menu": "Lietotāja izvēlne",
|
"User menu": "Lietotāja izvēlne",
|
||||||
"Username": "Lietotājvārds",
|
"Username": "Lietotājvārds",
|
||||||
"Video": "Video",
|
"Video": "Video",
|
||||||
|
|||||||
@@ -13,13 +13,9 @@
|
|||||||
"Version: {{version}}": "Wersja: {{version}}",
|
"Version: {{version}}": "Wersja: {{version}}",
|
||||||
"Username": "Nazwa użytkownika",
|
"Username": "Nazwa użytkownika",
|
||||||
"User menu": "Menu użytkownika",
|
"User menu": "Menu użytkownika",
|
||||||
"Unmute microphone": "Wyłącz wyciszenie mikrofonu",
|
|
||||||
"Turn on camera": "Włącz kamerę",
|
|
||||||
"Turn off camera": "Wyłącz kamerę",
|
|
||||||
"This call already exists, would you like to join?": "Te połączenie już istnieje, czy chcesz do niego dołączyć?",
|
"This call already exists, would you like to join?": "Te połączenie już istnieje, czy chcesz do niego dołączyć?",
|
||||||
"Take me Home": "Zabierz mnie do strony głównej",
|
"Take me Home": "Zabierz mnie do strony głównej",
|
||||||
"Submit feedback": "Prześlij opinię",
|
"Submit feedback": "Prześlij opinię",
|
||||||
"Stop sharing screen": "Zatrzymaj udostępnianie ekranu",
|
|
||||||
"Spotlight": "Centrum uwagi",
|
"Spotlight": "Centrum uwagi",
|
||||||
"Speaker": "Głośnik",
|
"Speaker": "Głośnik",
|
||||||
"Sign out": "Wyloguj się",
|
"Sign out": "Wyloguj się",
|
||||||
@@ -44,33 +40,25 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "Nie masz konta? <2>Utwórz je</2>",
|
"Not registered yet? <2>Create an account</2>": "Nie masz konta? <2>Utwórz je</2>",
|
||||||
"Not now, return to home screen": "Nie teraz, powróć do ekranu domowego",
|
"Not now, return to home screen": "Nie teraz, powróć do ekranu domowego",
|
||||||
"No": "Nie",
|
"No": "Nie",
|
||||||
"Mute microphone": "Wycisz mikrofon",
|
|
||||||
"More": "Więcej",
|
"More": "Więcej",
|
||||||
"Microphone": "Mikrofon",
|
"Microphone": "Mikrofon",
|
||||||
"Login to your account": "Zaloguj się do swojego konta",
|
"Login to your account": "Zaloguj się do swojego konta",
|
||||||
"Logging in…": "Logowanie…",
|
"Logging in…": "Logowanie…",
|
||||||
"Local volume": "Głośność lokalna",
|
"Local volume": "Głośność lokalna",
|
||||||
"Loading…": "Ładowanie…",
|
"Loading…": "Ładowanie…",
|
||||||
"Leave": "Opuść",
|
|
||||||
"Join existing call?": "Dołączyć do istniejącego połączenia?",
|
"Join existing call?": "Dołączyć do istniejącego połączenia?",
|
||||||
"Join call now": "Dołącz do połączenia teraz",
|
"Join call now": "Dołącz do połączenia teraz",
|
||||||
"Join call": "Dołącz do połączenia",
|
"Join call": "Dołącz do połączenia",
|
||||||
"Invite people": "Zaproś ludzi",
|
|
||||||
"Invite": "Zaproś",
|
|
||||||
"Inspector": "Inspektor",
|
"Inspector": "Inspektor",
|
||||||
"Incompatible versions!": "Niekompatybilne wersje!",
|
|
||||||
"Incompatible versions": "Niekompatybilne wersje",
|
"Incompatible versions": "Niekompatybilne wersje",
|
||||||
"Include debug logs": "Dołącz dzienniki debugowania",
|
"Include debug logs": "Dołącz dzienniki debugowania",
|
||||||
"Home": "Strona domowa",
|
"Home": "Strona domowa",
|
||||||
"Grid layout menu": "Menu układu siatki",
|
|
||||||
"Full screen": "Pełny ekran",
|
"Full screen": "Pełny ekran",
|
||||||
"Freedom": "Wolność",
|
|
||||||
"Fetching group call timed out.": "Przekroczono limit czasu na uzyskanie połączenia grupowego.",
|
"Fetching group call timed out.": "Przekroczono limit czasu na uzyskanie połączenia grupowego.",
|
||||||
"Exit full screen": "Opuść pełny ekran",
|
"Exit full screen": "Opuść pełny ekran",
|
||||||
"Download debug logs": "Pobierz dzienniki debugowania",
|
"Download debug logs": "Pobierz dzienniki debugowania",
|
||||||
"Display name": "Nazwa wyświetlana",
|
"Display name": "Nazwa wyświetlana",
|
||||||
"Developer": "Programista",
|
"Developer": "Programista",
|
||||||
"Details": "Szczegóły",
|
|
||||||
"Debug log request": "Prośba o dzienniki debugowania",
|
"Debug log request": "Prośba o dzienniki debugowania",
|
||||||
"Debug log": "Dzienniki debugowania",
|
"Debug log": "Dzienniki debugowania",
|
||||||
"Create account": "Utwórz konto",
|
"Create account": "Utwórz konto",
|
||||||
@@ -78,7 +66,6 @@
|
|||||||
"Copied!": "Skopiowano!",
|
"Copied!": "Skopiowano!",
|
||||||
"Confirm password": "Potwierdź hasło",
|
"Confirm password": "Potwierdź hasło",
|
||||||
"Close": "Zamknij",
|
"Close": "Zamknij",
|
||||||
"Change layout": "Zmień układ",
|
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
"Call type menu": "Menu typu połączenia",
|
"Call type menu": "Menu typu połączenia",
|
||||||
"Call link copied": "Skopiowano link do połączenia",
|
"Call link copied": "Skopiowano link do połączenia",
|
||||||
@@ -88,7 +75,6 @@
|
|||||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?</0><1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości</1>",
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Może zechcesz ustawić hasło, aby zachować swoje konto?</0><1>Będziesz w stanie utrzymać swoją nazwę i ustawić awatar do wyświetlania podczas połączeń w przyszłości</1>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Utwórz konto</0> lub <2>Dołącz jako gość</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Utwórz konto</0> lub <2>Dołącz jako gość</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Masz już konto?</0><1><0>Zaloguj się</0> lub <2>Dołącz jako gość</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Masz już konto?</0><1><0>Zaloguj się</0> lub <2>Dołącz jako gość</2></1>",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Copy": "Kopiuj",
|
"Copy": "Kopiuj",
|
||||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu.</0>",
|
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu.</0>",
|
||||||
"<0>Oops, something's gone wrong.</0>": "<0>Ojej, coś poszło nie tak.</0>",
|
"<0>Oops, something's gone wrong.</0>": "<0>Ojej, coś poszło nie tak.</0>",
|
||||||
@@ -117,7 +103,6 @@
|
|||||||
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Ta witryna jest chroniona przez ReCAPTCHA, więc obowiązują <2>Polityka prywatności</2> i <6>Warunki usług</6> Google. Klikając \"Zarejestruj\", zgadzasz się na naszą <12>Umowę licencyjną (EULA)</12>",
|
"This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>": "Ta witryna jest chroniona przez ReCAPTCHA, więc obowiązują <2>Polityka prywatności</2> i <6>Warunki usług</6> Google. Klikając \"Zarejestruj\", zgadzasz się na naszą <12>Umowę licencyjną (EULA)</12>",
|
||||||
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klikając \"Dołącz teraz do rozmowy\", zgadzasz się na naszą <2>Umowę licencyjną (EULA)</2>",
|
"By clicking \"Join call now\", you agree to our <2>End User Licensing Agreement (EULA)</2>": "Klikając \"Dołącz teraz do rozmowy\", zgadzasz się na naszą <2>Umowę licencyjną (EULA)</2>",
|
||||||
"End-to-end encryption isn't supported on your browser.": "Szyfrowanie end-to-end nie jest wspierane przez Twoją przeglądarkę.",
|
"End-to-end encryption isn't supported on your browser.": "Szyfrowanie end-to-end nie jest wspierane przez Twoją przeglądarkę.",
|
||||||
"Password (if none, E2EE is disabled)": "Hasło (brak oznacza, że E2EE jest włączone)",
|
|
||||||
"Retry sending logs": "Wyślij logi ponownie",
|
"Retry sending logs": "Wyślij logi ponownie",
|
||||||
"Thanks!": "Dziękujemy!",
|
"Thanks!": "Dziękujemy!",
|
||||||
"You were disconnected from the call": "Rozłączono Cię z połączenia",
|
"You were disconnected from the call": "Rozłączono Cię z połączenia",
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
"Register": "Зарегистрироваться",
|
"Register": "Зарегистрироваться",
|
||||||
"Registering…": "Регистрация…",
|
"Registering…": "Регистрация…",
|
||||||
"Logging in…": "Вход…",
|
"Logging in…": "Вход…",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Waiting for other participants…": "Ожидание других участников…",
|
"Waiting for other participants…": "Ожидание других участников…",
|
||||||
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
|
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
|
||||||
"Submit feedback": "Отправить отзыв",
|
"Submit feedback": "Отправить отзыв",
|
||||||
"Sending debug logs…": "Отправка журнала отладки…",
|
"Sending debug logs…": "Отправка журнала отладки…",
|
||||||
"Select an option": "Выберите вариант",
|
"Select an option": "Выберите вариант",
|
||||||
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Другие пользователи пытаются присоединиться с неподдерживаемых версий программы. Этим участникам надо перезагрузить браузер: <1>{userLis}</1>",
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Другие пользователи пытаются присоединиться с неподдерживаемых версий программы. Этим участникам надо перезагрузить браузер: <1>{userLis}</1>",
|
||||||
"Grid layout menu": "Меню \"Расположение сеткой\"",
|
|
||||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?</0><1>Так вы можете оставить своё имя и задать аватар для будущих звонков.</1>",
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Почему бы не задать пароль, тем самым сохранив аккаунт?</0><1>Так вы можете оставить своё имя и задать аватар для будущих звонков.</1>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Создать аккаунт</0> или <2>Зайти как гость</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Создать аккаунт</0> или <2>Зайти как гость</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже есть аккаунт?</0><1><0>Войти с ним</0> или <2>Зайти как гость</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже есть аккаунт?</0><1><0>Войти с ним</0> или <2>Зайти как гость</2></1>",
|
||||||
@@ -24,11 +22,7 @@
|
|||||||
"Version: {{version}}": "Версия: {{version}}",
|
"Version: {{version}}": "Версия: {{version}}",
|
||||||
"Username": "Имя пользователя",
|
"Username": "Имя пользователя",
|
||||||
"User menu": "Меню пользователя",
|
"User menu": "Меню пользователя",
|
||||||
"Unmute microphone": "Включить микрофон",
|
|
||||||
"Turn on camera": "Включить камеру",
|
|
||||||
"Turn off camera": "Отключить камеру",
|
|
||||||
"Take me Home": "Перейти в Начало",
|
"Take me Home": "Перейти в Начало",
|
||||||
"Stop sharing screen": "Остановить показ экрана",
|
|
||||||
"Spotlight": "Внимание",
|
"Spotlight": "Внимание",
|
||||||
"Speaker": "Динамик",
|
"Speaker": "Динамик",
|
||||||
"Sign out": "Выйти",
|
"Sign out": "Выйти",
|
||||||
@@ -55,36 +49,28 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "Ещё не зарегистрированы? <2>Создайте аккаунт</2>",
|
"Not registered yet? <2>Create an account</2>": "Ещё не зарегистрированы? <2>Создайте аккаунт</2>",
|
||||||
"Not now, return to home screen": "Не сейчас, вернуться в Начало",
|
"Not now, return to home screen": "Не сейчас, вернуться в Начало",
|
||||||
"No": "Нет",
|
"No": "Нет",
|
||||||
"Mute microphone": "Отключить микрофон",
|
|
||||||
"More": "Больше",
|
"More": "Больше",
|
||||||
"Microphone": "Микрофон",
|
"Microphone": "Микрофон",
|
||||||
"Login to your account": "Войдите в свой аккаунт",
|
"Login to your account": "Войдите в свой аккаунт",
|
||||||
"Login": "Вход",
|
"Login": "Вход",
|
||||||
"Loading…": "Загрузка…",
|
"Loading…": "Загрузка…",
|
||||||
"Leave": "Покинуть",
|
|
||||||
"Join existing call?": "Присоединиться к существующему звонку?",
|
"Join existing call?": "Присоединиться к существующему звонку?",
|
||||||
"Join call now": "Присоединиться сейчас",
|
"Join call now": "Присоединиться сейчас",
|
||||||
"Join call": "Присоединиться",
|
"Join call": "Присоединиться",
|
||||||
"Invite people": "Пригласить участников",
|
|
||||||
"Invite": "Пригласить",
|
|
||||||
"Inspector": "Инспектор",
|
"Inspector": "Инспектор",
|
||||||
"Incompatible versions!": "Несовместимые версии!",
|
|
||||||
"Incompatible versions": "Несовместимые версии",
|
"Incompatible versions": "Несовместимые версии",
|
||||||
"Home": "Начало",
|
"Home": "Начало",
|
||||||
"Go": "Далее",
|
"Go": "Далее",
|
||||||
"Full screen": "Полноэкранный режим",
|
"Full screen": "Полноэкранный режим",
|
||||||
"Freedom": "Свобода",
|
|
||||||
"Fetching group call timed out.": "Истекло время ожидания для группового звонка.",
|
"Fetching group call timed out.": "Истекло время ожидания для группового звонка.",
|
||||||
"Exit full screen": "Выйти из полноэкранного режима",
|
"Exit full screen": "Выйти из полноэкранного режима",
|
||||||
"Display name": "Видимое имя",
|
"Display name": "Видимое имя",
|
||||||
"Developer": "Разработчику",
|
"Developer": "Разработчику",
|
||||||
"Details": "Подробности",
|
|
||||||
"Create account": "Создать аккаунт",
|
"Create account": "Создать аккаунт",
|
||||||
"Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок",
|
"Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок",
|
||||||
"Copied!": "Скопировано!",
|
"Copied!": "Скопировано!",
|
||||||
"Confirm password": "Подтвердите пароль",
|
"Confirm password": "Подтвердите пароль",
|
||||||
"Close": "Закрыть",
|
"Close": "Закрыть",
|
||||||
"Change layout": "Изменить расположение",
|
|
||||||
"Camera": "Камера",
|
"Camera": "Камера",
|
||||||
"Call link copied": "Ссылка на звонок скопирована",
|
"Call link copied": "Ссылка на звонок скопирована",
|
||||||
"Avatar": "Аватар",
|
"Avatar": "Аватар",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"Waiting for other participants…": "Čaká sa na ďalších účastníkov…",
|
"Waiting for other participants…": "Čaká sa na ďalších účastníkov…",
|
||||||
"Take me Home": "Zober ma domov",
|
"Take me Home": "Zober ma domov",
|
||||||
"Submit feedback": "Odoslať spätnú väzbu",
|
"Submit feedback": "Odoslať spätnú väzbu",
|
||||||
"Stop sharing screen": "Zastaviť zdieľanie obrazovky",
|
|
||||||
"Show call inspector": "Zobraziť inšpektora hovorov",
|
"Show call inspector": "Zobraziť inšpektora hovorov",
|
||||||
"Share screen": "Zdieľať obrazovku",
|
"Share screen": "Zdieľať obrazovku",
|
||||||
"Sending…": "Odosielanie…",
|
"Sending…": "Odosielanie…",
|
||||||
@@ -27,27 +26,20 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "Ešte nie ste zaregistrovaný? <2>Vytvorte si účet</2>",
|
"Not registered yet? <2>Create an account</2>": "Ešte nie ste zaregistrovaný? <2>Vytvorte si účet</2>",
|
||||||
"Not now, return to home screen": "Teraz nie, vrátiť sa na domovskú obrazovku",
|
"Not now, return to home screen": "Teraz nie, vrátiť sa na domovskú obrazovku",
|
||||||
"No": "Nie",
|
"No": "Nie",
|
||||||
"Mute microphone": "Stlmiť mikrofón",
|
|
||||||
"More": "Viac",
|
"More": "Viac",
|
||||||
"Microphone": "Mikrofón",
|
"Microphone": "Mikrofón",
|
||||||
"Login to your account": "Prihláste sa do svojho konta",
|
"Login to your account": "Prihláste sa do svojho konta",
|
||||||
"Login": "Prihlásiť sa",
|
"Login": "Prihlásiť sa",
|
||||||
"Logging in…": "Prihlasovanie…",
|
"Logging in…": "Prihlasovanie…",
|
||||||
"Loading…": "Načítanie…",
|
"Loading…": "Načítanie…",
|
||||||
"Leave": "Opustiť",
|
|
||||||
"Join existing call?": "Pripojiť sa k existujúcemu hovoru?",
|
"Join existing call?": "Pripojiť sa k existujúcemu hovoru?",
|
||||||
"Join call now": "Pripojiť sa k hovoru teraz",
|
"Join call now": "Pripojiť sa k hovoru teraz",
|
||||||
"Join call": "Pripojiť sa k hovoru",
|
"Join call": "Pripojiť sa k hovoru",
|
||||||
"Invite people": "Pozvať ľudí",
|
|
||||||
"Invite": "Pozvať",
|
|
||||||
"Inspector": "Inšpektor",
|
"Inspector": "Inšpektor",
|
||||||
"Incompatible versions!": "Nekompatibilné verzie!",
|
|
||||||
"Incompatible versions": "Nekompatibilné verzie",
|
"Incompatible versions": "Nekompatibilné verzie",
|
||||||
"Home": "Domov",
|
"Home": "Domov",
|
||||||
"Grid layout menu": "Ponuka rozloženia mriežky",
|
|
||||||
"Go": "Prejsť",
|
"Go": "Prejsť",
|
||||||
"Full screen": "Zobrazenie na celú obrazovku",
|
"Full screen": "Zobrazenie na celú obrazovku",
|
||||||
"Freedom": "Sloboda",
|
|
||||||
"Exit full screen": "Ukončiť zobrazenie na celú obrazovku",
|
"Exit full screen": "Ukončiť zobrazenie na celú obrazovku",
|
||||||
"Download debug logs": "Stiahnuť záznamy ladenia",
|
"Download debug logs": "Stiahnuť záznamy ladenia",
|
||||||
"Your recent calls": "Vaše nedávne hovory",
|
"Your recent calls": "Vaše nedávne hovory",
|
||||||
@@ -61,9 +53,6 @@
|
|||||||
"Version: {{version}}": "Verzia: {{version}}",
|
"Version: {{version}}": "Verzia: {{version}}",
|
||||||
"Username": "Meno používateľa",
|
"Username": "Meno používateľa",
|
||||||
"User menu": "Používateľské menu",
|
"User menu": "Používateľské menu",
|
||||||
"Unmute microphone": "Zrušiť stlmenie mikrofónu",
|
|
||||||
"Turn on camera": "Zapnúť kameru",
|
|
||||||
"Turn off camera": "Vypnúť kameru",
|
|
||||||
"This call already exists, would you like to join?": "Tento hovor už existuje, chceli by ste sa k nemu pripojiť?",
|
"This call already exists, would you like to join?": "Tento hovor už existuje, chceli by ste sa k nemu pripojiť?",
|
||||||
"Speaker": "Reproduktor",
|
"Speaker": "Reproduktor",
|
||||||
"Sign out": "Odhlásiť sa",
|
"Sign out": "Odhlásiť sa",
|
||||||
@@ -71,7 +60,6 @@
|
|||||||
"Settings": "Nastavenia",
|
"Settings": "Nastavenia",
|
||||||
"Display name": "Zobrazované meno",
|
"Display name": "Zobrazované meno",
|
||||||
"Developer": "Vývojár",
|
"Developer": "Vývojár",
|
||||||
"Details": "Podrobnosti",
|
|
||||||
"Debug log request": "Žiadosť o záznam ladenia",
|
"Debug log request": "Žiadosť o záznam ladenia",
|
||||||
"Debug log": "Záznam o ladení",
|
"Debug log": "Záznam o ladení",
|
||||||
"Create account": "Vytvoriť účet",
|
"Create account": "Vytvoriť účet",
|
||||||
@@ -80,7 +68,6 @@
|
|||||||
"Copied!": "Skopírované!",
|
"Copied!": "Skopírované!",
|
||||||
"Confirm password": "Potvrdiť heslo",
|
"Confirm password": "Potvrdiť heslo",
|
||||||
"Close": "Zatvoriť",
|
"Close": "Zatvoriť",
|
||||||
"Change layout": "Zmeniť rozloženie",
|
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
"Call type menu": "Ponuka typu hovoru",
|
"Call type menu": "Ponuka typu hovoru",
|
||||||
"Call link copied": "Odkaz na hovor skopírovaný",
|
"Call link copied": "Odkaz na hovor skopírovaný",
|
||||||
@@ -91,7 +78,6 @@
|
|||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Pripojiť sa k hovoru teraz</0><1>alebo</1><2>Kopírovať odkaz na hovor a pripojiť sa neskôr</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Pripojiť sa k hovoru teraz</0><1>alebo</1><2>Kopírovať odkaz na hovor a pripojiť sa neskôr</2>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Vytvoriť konto</0> Alebo <2>Prihlásiť sa ako hosť</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Vytvoriť konto</0> Alebo <2>Prihlásiť sa ako hosť</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Už máte konto?</0><1><0>Prihláste sa</0> Alebo <2>Prihlásiť sa ako hosť</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Už máte konto?</0><1><0>Prihláste sa</0> Alebo <2>Prihlásiť sa ako hosť</2></1>",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.</0>",
|
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.</0>",
|
||||||
"<0>Oops, something's gone wrong.</0>": "<0>Hups, niečo sa pokazilo.</0>",
|
"<0>Oops, something's gone wrong.</0>": "<0>Hups, niečo sa pokazilo.</0>",
|
||||||
"Expose developer settings in the settings window.": "Zobraziť nastavenia pre vývojárov v okne nastavení.",
|
"Expose developer settings in the settings window.": "Zobraziť nastavenia pre vývojárov v okne nastavení.",
|
||||||
@@ -122,6 +108,5 @@
|
|||||||
"Thanks!": "Ďakujeme!",
|
"Thanks!": "Ďakujeme!",
|
||||||
"You were disconnected from the call": "Boli ste odpojení z hovoru",
|
"You were disconnected from the call": "Boli ste odpojení z hovoru",
|
||||||
"Enable end-to-end encryption (password protected calls)": "Povoliť end-to-end šifrovanie (hovory chránené heslom)",
|
"Enable end-to-end encryption (password protected calls)": "Povoliť end-to-end šifrovanie (hovory chránené heslom)",
|
||||||
"End-to-end encryption isn't supported on your browser.": "End-to-end šifrovanie nie je vo vašom prehliadači podporované.",
|
"End-to-end encryption isn't supported on your browser.": "End-to-end šifrovanie nie je vo vašom prehliadači podporované."
|
||||||
"Password (if none, E2EE is disabled)": "Heslo (ak nie je, šifrovanie je vypnuté)"
|
|
||||||
}
|
}
|
||||||
|
|||||||
1
public/locales/sv/app.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
"Call link copied": "Arama bağlantısı kopyalandı",
|
"Call link copied": "Arama bağlantısı kopyalandı",
|
||||||
"Call type menu": "Arama tipi menüsü",
|
"Call type menu": "Arama tipi menüsü",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
"Change layout": "Yerleşimi değiştir",
|
|
||||||
"Close": "Kapat",
|
"Close": "Kapat",
|
||||||
"Confirm password": "Parolayı tekrar edin",
|
"Confirm password": "Parolayı tekrar edin",
|
||||||
"Copied!": "Kopyalandı",
|
"Copied!": "Kopyalandı",
|
||||||
@@ -14,26 +13,20 @@
|
|||||||
"Create account": "Hesap aç",
|
"Create account": "Hesap aç",
|
||||||
"Debug log": "Hata ayıklama kütüğü",
|
"Debug log": "Hata ayıklama kütüğü",
|
||||||
"Debug log request": "Hata ayıklama kütük istemi",
|
"Debug log request": "Hata ayıklama kütük istemi",
|
||||||
"Details": "Ayrıntı",
|
|
||||||
"Developer": "Geliştirici",
|
"Developer": "Geliştirici",
|
||||||
"Display name": "Ekran adı",
|
"Display name": "Ekran adı",
|
||||||
"Download debug logs": "Hata ayıklama kütüğünü indir",
|
"Download debug logs": "Hata ayıklama kütüğünü indir",
|
||||||
"Exit full screen": "Tam ekranı terk et",
|
"Exit full screen": "Tam ekranı terk et",
|
||||||
"Fetching group call timed out.": "Grup çağrısı zaman aşımına uğradı.",
|
"Fetching group call timed out.": "Grup çağrısı zaman aşımına uğradı.",
|
||||||
"Freedom": "Özgürlük",
|
|
||||||
"Full screen": "Tam ekran",
|
"Full screen": "Tam ekran",
|
||||||
"Go": "Git",
|
"Go": "Git",
|
||||||
"Grid layout menu": "Izgara plan menü",
|
|
||||||
"Home": "Ev",
|
"Home": "Ev",
|
||||||
"Include debug logs": "Hata ayıklama kütüğünü dahil et",
|
"Include debug logs": "Hata ayıklama kütüğünü dahil et",
|
||||||
"Incompatible versions": "Uyumsuz sürümler",
|
"Incompatible versions": "Uyumsuz sürümler",
|
||||||
"Incompatible versions!": "Sürüm uyumsuz!",
|
|
||||||
"Inspector": "Denetçi",
|
"Inspector": "Denetçi",
|
||||||
"Invite people": "Kişileri davet et",
|
|
||||||
"Join call": "Aramaya katıl",
|
"Join call": "Aramaya katıl",
|
||||||
"Join call now": "Aramaya katıl",
|
"Join call now": "Aramaya katıl",
|
||||||
"Join existing call?": "Mevcut aramaya katıl?",
|
"Join existing call?": "Mevcut aramaya katıl?",
|
||||||
"Leave": "Çık",
|
|
||||||
"Loading…": "Yükleniyor…",
|
"Loading…": "Yükleniyor…",
|
||||||
"Local volume": "Yerel ses seviyesi",
|
"Local volume": "Yerel ses seviyesi",
|
||||||
"Logging in…": "Giriliyor…",
|
"Logging in…": "Giriliyor…",
|
||||||
@@ -41,7 +34,6 @@
|
|||||||
"Login to your account": "Hesabınıza girin",
|
"Login to your account": "Hesabınıza girin",
|
||||||
"Microphone": "Mikrofon",
|
"Microphone": "Mikrofon",
|
||||||
"More": "Daha",
|
"More": "Daha",
|
||||||
"Mute microphone": "Mikrofonu kapat",
|
|
||||||
"No": "Hayır",
|
"No": "Hayır",
|
||||||
"Not now, return to home screen": "Şimdi değil, ev ekranına dön",
|
"Not now, return to home screen": "Şimdi değil, ev ekranına dön",
|
||||||
"Not registered yet? <2>Create an account</2>": "Kaydolmadınız mı? <2>Hesap açın</2>",
|
"Not registered yet? <2>Create an account</2>": "Kaydolmadınız mı? <2>Hesap açın</2>",
|
||||||
@@ -62,11 +54,9 @@
|
|||||||
"Show call inspector": "Arama denetçisini göster",
|
"Show call inspector": "Arama denetçisini göster",
|
||||||
"Sign in": "Gir",
|
"Sign in": "Gir",
|
||||||
"Sign out": "Çık",
|
"Sign out": "Çık",
|
||||||
"Stop sharing screen": "Ekran paylaşmayı terk et",
|
|
||||||
"Submit feedback": "Geri bildirim ver",
|
"Submit feedback": "Geri bildirim ver",
|
||||||
"Take me Home": "Ev ekranına gir",
|
"Take me Home": "Ev ekranına gir",
|
||||||
"This call already exists, would you like to join?": "Bu arama zaten var, katılmak ister misiniz?",
|
"This call already exists, would you like to join?": "Bu arama zaten var, katılmak ister misiniz?",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Hesap oluştur</0> yahut <2>Konuk olarak gir</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Hesap oluştur</0> yahut <2>Konuk olarak gir</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Mevcut hesabınız mı var?</0><1><0>Gir</0> yahut <2>Konuk girişi</2></1>"
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Mevcut hesabınız mı var?</0><1><0>Gir</0> yahut <2>Konuk girişi</2></1>"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,9 @@
|
|||||||
"Version: {{version}}": "Версія: {{version}}",
|
"Version: {{version}}": "Версія: {{version}}",
|
||||||
"Username": "Ім'я користувача",
|
"Username": "Ім'я користувача",
|
||||||
"User menu": "Меню користувача",
|
"User menu": "Меню користувача",
|
||||||
"Unmute microphone": "Увімкнути мікрофон",
|
|
||||||
"Turn on camera": "Увімкнути камеру",
|
|
||||||
"Turn off camera": "Вимкнути камеру",
|
|
||||||
"This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?",
|
"This call already exists, would you like to join?": "Цей виклик уже існує, бажаєте приєднатися?",
|
||||||
"Take me Home": "Перейти до Домівки",
|
"Take me Home": "Перейти до Домівки",
|
||||||
"Submit feedback": "Надіслати відгук",
|
"Submit feedback": "Надіслати відгук",
|
||||||
"Stop sharing screen": "Припинити показ екрана",
|
|
||||||
"Spotlight": "У центрі уваги",
|
"Spotlight": "У центрі уваги",
|
||||||
"Speaker": "Динамік",
|
"Speaker": "Динамік",
|
||||||
"Sign out": "Вийти",
|
"Sign out": "Вийти",
|
||||||
@@ -43,34 +39,26 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "Ще не зареєстровані? <2>Створіть обліковий запис</2>",
|
"Not registered yet? <2>Create an account</2>": "Ще не зареєстровані? <2>Створіть обліковий запис</2>",
|
||||||
"Not now, return to home screen": "Не зараз, повернутися на екран домівки",
|
"Not now, return to home screen": "Не зараз, повернутися на екран домівки",
|
||||||
"No": "Ні",
|
"No": "Ні",
|
||||||
"Mute microphone": "Заглушити мікрофон",
|
|
||||||
"More": "Докладніше",
|
"More": "Докладніше",
|
||||||
"Microphone": "Мікрофон",
|
"Microphone": "Мікрофон",
|
||||||
"Login to your account": "Увійдіть до свого облікового запису",
|
"Login to your account": "Увійдіть до свого облікового запису",
|
||||||
"Login": "Увійти",
|
"Login": "Увійти",
|
||||||
"Logging in…": "Вхід…",
|
"Logging in…": "Вхід…",
|
||||||
"Local volume": "Локальна гучність",
|
"Local volume": "Локальна гучність",
|
||||||
"Leave": "Вийти",
|
|
||||||
"Join existing call?": "Приєднатися до наявного виклику?",
|
"Join existing call?": "Приєднатися до наявного виклику?",
|
||||||
"Join call now": "Приєднатися до виклику зараз",
|
"Join call now": "Приєднатися до виклику зараз",
|
||||||
"Join call": "Приєднатися до виклику",
|
"Join call": "Приєднатися до виклику",
|
||||||
"Invite people": "Запросити людей",
|
|
||||||
"Invite": "Запросити",
|
|
||||||
"Inspector": "Інспектор",
|
"Inspector": "Інспектор",
|
||||||
"Incompatible versions!": "Несумісні версії!",
|
|
||||||
"Incompatible versions": "Несумісні версії",
|
"Incompatible versions": "Несумісні версії",
|
||||||
"Include debug logs": "Долучити журнали налагодження",
|
"Include debug logs": "Долучити журнали налагодження",
|
||||||
"Home": "Домівка",
|
"Home": "Домівка",
|
||||||
"Grid layout menu": "Меню у вигляді сітки",
|
|
||||||
"Go": "Далі",
|
"Go": "Далі",
|
||||||
"Full screen": "Повноекранний режим",
|
"Full screen": "Повноекранний режим",
|
||||||
"Freedom": "Свобода",
|
|
||||||
"Fetching group call timed out.": "Вичерпано час очікування групового виклику.",
|
"Fetching group call timed out.": "Вичерпано час очікування групового виклику.",
|
||||||
"Exit full screen": "Вийти з повноекранного режиму",
|
"Exit full screen": "Вийти з повноекранного режиму",
|
||||||
"Download debug logs": "Завантажити журнали налагодження",
|
"Download debug logs": "Завантажити журнали налагодження",
|
||||||
"Display name": "Псевдонім",
|
"Display name": "Псевдонім",
|
||||||
"Developer": "Розробнику",
|
"Developer": "Розробнику",
|
||||||
"Details": "Подробиці",
|
|
||||||
"Debug log request": "Запит журналу налагодження",
|
"Debug log request": "Запит журналу налагодження",
|
||||||
"Debug log": "Журнал налагодження",
|
"Debug log": "Журнал налагодження",
|
||||||
"Create account": "Створити обліковий запис",
|
"Create account": "Створити обліковий запис",
|
||||||
@@ -78,7 +66,6 @@
|
|||||||
"Copied!": "Скопійовано!",
|
"Copied!": "Скопійовано!",
|
||||||
"Confirm password": "Підтвердити пароль",
|
"Confirm password": "Підтвердити пароль",
|
||||||
"Close": "Закрити",
|
"Close": "Закрити",
|
||||||
"Change layout": "Змінити макет",
|
|
||||||
"Camera": "Камера",
|
"Camera": "Камера",
|
||||||
"Call type menu": "Меню типу виклику",
|
"Call type menu": "Меню типу виклику",
|
||||||
"Call link copied": "Посилання на виклик скопійовано",
|
"Call link copied": "Посилання на виклик скопійовано",
|
||||||
@@ -88,7 +75,6 @@
|
|||||||
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Чому б не завершити, налаштувавши пароль для збереження свого облікового запису?</0><1>Ви зможете зберегти своє ім'я та встановити аватарку для подальшого користування під час майбутніх викликів</1>",
|
"<0>Why not finish by setting up a password to keep your account?</0><1>You'll be able to keep your name and set an avatar for use on future calls</1>": "<0>Чому б не завершити, налаштувавши пароль для збереження свого облікового запису?</0><1>Ви зможете зберегти своє ім'я та встановити аватарку для подальшого користування під час майбутніх викликів</1>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Створити обліковий запис</0> або <2>Отримати доступ як гість</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Створити обліковий запис</0> або <2>Отримати доступ як гість</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже маєте обліковий запис?</0><1><0>Увійти</0> Or <2>Отримати доступ як гість</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Уже маєте обліковий запис?</0><1><0>Увійти</0> Or <2>Отримати доступ як гість</2></1>",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Приєднатися до виклику зараз</0><1>Or</1><2>Скопіювати посилання на виклик і приєднатися пізніше</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Приєднатися до виклику зараз</0><1>Or</1><2>Скопіювати посилання на виклик і приєднатися пізніше</2>",
|
||||||
"Element Call Home": "Домівка Element Call",
|
"Element Call Home": "Домівка Element Call",
|
||||||
"Copy": "Копіювати",
|
"Copy": "Копіювати",
|
||||||
@@ -123,5 +109,18 @@
|
|||||||
"Thanks!": "Дякуємо!",
|
"Thanks!": "Дякуємо!",
|
||||||
"Enable end-to-end encryption (password protected calls)": "Увімкнути наскрізне шифрування (захищені паролем виклики)",
|
"Enable end-to-end encryption (password protected calls)": "Увімкнути наскрізне шифрування (захищені паролем виклики)",
|
||||||
"End-to-end encryption isn't supported on your browser.": "Наскрізне шифрування не підтримується вашим переглядачем.",
|
"End-to-end encryption isn't supported on your browser.": "Наскрізне шифрування не підтримується вашим переглядачем.",
|
||||||
"Password (if none, E2EE is disabled)": "Пароль (якщо немає, наскрізне шифрування вимкнено)"
|
"{{count, number}}|other": "{{count, number}}",
|
||||||
|
"{{names, list(style: short;)}}": "{{names, list(style: short;)}}",
|
||||||
|
"Encrypted": "Зашифровано",
|
||||||
|
"Microphone on": "Мікрофон увімкнено",
|
||||||
|
"Not encrypted": "Не зашифровано",
|
||||||
|
"Share": "Поділитися",
|
||||||
|
"Sharing screen": "Презентація екрана",
|
||||||
|
"Video off": "Відео вимкнено",
|
||||||
|
"Video on": "Відео ввімкнено",
|
||||||
|
"{{count, number}}|one": "{{count, number}}",
|
||||||
|
"End call": "Завершити виклик",
|
||||||
|
"Grid": "Сітка",
|
||||||
|
"Microphone off": "Мікрофон вимкнено",
|
||||||
|
"Share this call": "Поділитися цим викликом"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Login": "Đăng nhập",
|
"Login": "Đăng nhập",
|
||||||
"Join call": "Tham gia cuộc gọi",
|
"Join call": "Tham gia cuộc gọi",
|
||||||
"Mute microphone": "Tắt micrô",
|
|
||||||
"Password": "Mật khẩu",
|
"Password": "Mật khẩu",
|
||||||
"Settings": "Cài đặt",
|
"Settings": "Cài đặt",
|
||||||
"Sending…": "Đang gửi…",
|
"Sending…": "Đang gửi…",
|
||||||
@@ -17,18 +16,13 @@
|
|||||||
"WebRTC is not supported or is being blocked in this browser.": "WebRTC không được hỗ trợ hay bị chặn trong trình duyệt này.",
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC không được hỗ trợ hay bị chặn trong trình duyệt này.",
|
||||||
"Waiting for other participants…": "Đang đợi những người khác…",
|
"Waiting for other participants…": "Đang đợi những người khác…",
|
||||||
"Version: {{version}}": "Phiên bản: {{version}}",
|
"Version: {{version}}": "Phiên bản: {{version}}",
|
||||||
"Turn on camera": "Bật máy quay",
|
|
||||||
"Turn off camera": "Tắt máy quay",
|
|
||||||
"Submit feedback": "Gửi phản hồi",
|
"Submit feedback": "Gửi phản hồi",
|
||||||
"Stop sharing screen": "Ngừng chia sẻ màn hình",
|
|
||||||
"Speaker": "Loa",
|
"Speaker": "Loa",
|
||||||
"Sign out": "Đăng xuất",
|
"Sign out": "Đăng xuất",
|
||||||
"Share screen": "Chia sẻ màn hình",
|
"Share screen": "Chia sẻ màn hình",
|
||||||
"No": "Không",
|
"No": "Không",
|
||||||
"Invite people": "Mời mọi người",
|
|
||||||
"Join call now": "Tham gia cuộc gọi",
|
"Join call now": "Tham gia cuộc gọi",
|
||||||
"Create account": "Tạo tài khoản",
|
"Create account": "Tạo tài khoản",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Tạo tài khoản</0> Hay <2>Tham gia dưới tên khác</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Tạo tài khoản</0> Hay <2>Tham gia dưới tên khác</2>",
|
||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Tham gia cuộc gọi</0><1>hay</1><2>Sao chép liên kết cuộc gọi và tham gia sau</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Tham gia cuộc gọi</0><1>hay</1><2>Sao chép liên kết cuộc gọi và tham gia sau</2>",
|
||||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Gửi nhật ký gỡ lỗi sẽ giúp chúng tôi theo dõi vấn đề.</0>",
|
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>Gửi nhật ký gỡ lỗi sẽ giúp chúng tôi theo dõi vấn đề.</0>",
|
||||||
@@ -39,23 +33,18 @@
|
|||||||
"Copied!": "Đã sao chép!",
|
"Copied!": "Đã sao chép!",
|
||||||
"Confirm password": "Xác nhận mật khẩu",
|
"Confirm password": "Xác nhận mật khẩu",
|
||||||
"Close": "Đóng",
|
"Close": "Đóng",
|
||||||
"Change layout": "Thay đổi bố cục",
|
|
||||||
"Debug log": "Nhật ký gỡ lỗi",
|
"Debug log": "Nhật ký gỡ lỗi",
|
||||||
"Copy": "Sao chép",
|
"Copy": "Sao chép",
|
||||||
"Copy and share this call link": "Sao chép và chia sẻ liên kết cuộc gọi này",
|
"Copy and share this call link": "Sao chép và chia sẻ liên kết cuộc gọi này",
|
||||||
"Display name": "Tên hiển thị",
|
"Display name": "Tên hiển thị",
|
||||||
"Developer Settings": "Cài đặt phát triển",
|
"Developer Settings": "Cài đặt phát triển",
|
||||||
"Developer": "Nhà phát triển",
|
"Developer": "Nhà phát triển",
|
||||||
"Details": "Chi tiết",
|
|
||||||
"Download debug logs": "Tải xuống nhật ký gỡ lỗi",
|
"Download debug logs": "Tải xuống nhật ký gỡ lỗi",
|
||||||
"Feedback": "Phản hồi",
|
"Feedback": "Phản hồi",
|
||||||
"Full screen": "Toàn màn hình",
|
"Full screen": "Toàn màn hình",
|
||||||
"Incompatible versions!": "Phiên bản không tương thích!",
|
|
||||||
"Incompatible versions": "Phiên bản không tương thích",
|
"Incompatible versions": "Phiên bản không tương thích",
|
||||||
"Include debug logs": "Kèm theo nhật ký gỡ lỗi",
|
"Include debug logs": "Kèm theo nhật ký gỡ lỗi",
|
||||||
"Invite": "Mời",
|
|
||||||
"Join existing call?": "Tham gia cuộc gọi?",
|
"Join existing call?": "Tham gia cuộc gọi?",
|
||||||
"Leave": "Rời",
|
|
||||||
"Loading…": "Đang tải…",
|
"Loading…": "Đang tải…",
|
||||||
"Logging in…": "Đang đăng nhập…",
|
"Logging in…": "Đang đăng nhập…",
|
||||||
"Login to your account": "Đăng nhập vào tài khoản của bạn",
|
"Login to your account": "Đăng nhập vào tài khoản của bạn",
|
||||||
@@ -72,7 +61,6 @@
|
|||||||
"Exit full screen": "Rời chế độ toàn màn hình",
|
"Exit full screen": "Rời chế độ toàn màn hình",
|
||||||
"Profile": "Hồ sơ",
|
"Profile": "Hồ sơ",
|
||||||
"Registering…": "Đang đăng ký…",
|
"Registering…": "Đang đăng ký…",
|
||||||
"Unmute microphone": "Bật micrô",
|
|
||||||
"This call already exists, would you like to join?": "Cuộc gọi đã tồn tại, bạn có muốn tham gia không?",
|
"This call already exists, would you like to join?": "Cuộc gọi đã tồn tại, bạn có muốn tham gia không?",
|
||||||
"Recaptcha not loaded": "Chưa tải được Recaptcha",
|
"Recaptcha not loaded": "Chưa tải được Recaptcha",
|
||||||
"Debug log request": "Yêu cầu nhật ký gỡ lỗi",
|
"Debug log request": "Yêu cầu nhật ký gỡ lỗi",
|
||||||
|
|||||||
@@ -11,13 +11,9 @@
|
|||||||
"Version: {{version}}": "版本:{{version}}",
|
"Version: {{version}}": "版本:{{version}}",
|
||||||
"Username": "用户名",
|
"Username": "用户名",
|
||||||
"User menu": "用户菜单",
|
"User menu": "用户菜单",
|
||||||
"Unmute microphone": "取消麦克风静音",
|
|
||||||
"Turn on camera": "开启摄像头",
|
|
||||||
"Turn off camera": "关闭摄像头",
|
|
||||||
"This call already exists, would you like to join?": "该通话已存在,你想加入吗?",
|
"This call already exists, would you like to join?": "该通话已存在,你想加入吗?",
|
||||||
"Take me Home": "返回主页",
|
"Take me Home": "返回主页",
|
||||||
"Submit feedback": "提交反馈",
|
"Submit feedback": "提交反馈",
|
||||||
"Stop sharing screen": "停止屏幕共享",
|
|
||||||
"Spotlight": "聚焦模式",
|
"Spotlight": "聚焦模式",
|
||||||
"Speaker": "发言人",
|
"Speaker": "发言人",
|
||||||
"Sign out": "注销登录",
|
"Sign out": "注销登录",
|
||||||
@@ -28,7 +24,6 @@
|
|||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>现在加入通话</0><1>或</1><2>复制通话链接并稍后加入</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>现在加入通话</0><1>或</1><2>复制通话链接并稍后加入</2>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>创建账户</0> Or <2>以访客身份继续</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>创建账户</0> Or <2>以访客身份继续</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>已有账户?</0><1><0>登录</0> Or <2>以访客身份继续</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>已有账户?</0><1><0>登录</0> Or <2>以访客身份继续</2></1>",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Inspector": "检查器",
|
"Inspector": "检查器",
|
||||||
"Show call inspector": "显示通话检查器",
|
"Show call inspector": "显示通话检查器",
|
||||||
"Share screen": "屏幕共享",
|
"Share screen": "屏幕共享",
|
||||||
@@ -50,7 +45,6 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "还没有注册? <2>创建账户<2>",
|
"Not registered yet? <2>Create an account</2>": "还没有注册? <2>创建账户<2>",
|
||||||
"Not now, return to home screen": "暂不,先返回主页",
|
"Not now, return to home screen": "暂不,先返回主页",
|
||||||
"No": "否",
|
"No": "否",
|
||||||
"Mute microphone": "麦克风静音",
|
|
||||||
"More": "更多",
|
"More": "更多",
|
||||||
"Microphone": "麦克风",
|
"Microphone": "麦克风",
|
||||||
"Login to your account": "登录你的账户",
|
"Login to your account": "登录你的账户",
|
||||||
@@ -58,27 +52,20 @@
|
|||||||
"Logging in…": "登录中……",
|
"Logging in…": "登录中……",
|
||||||
"Local volume": "本地音量",
|
"Local volume": "本地音量",
|
||||||
"Loading…": "加载中……",
|
"Loading…": "加载中……",
|
||||||
"Leave": "离开",
|
|
||||||
"Join existing call?": "加入现有的通话?",
|
"Join existing call?": "加入现有的通话?",
|
||||||
"Join call now": "现在加入通话",
|
"Join call now": "现在加入通话",
|
||||||
"Join call": "加入通话",
|
"Join call": "加入通话",
|
||||||
"Invite people": "邀请他人",
|
|
||||||
"Invite": "邀请",
|
|
||||||
"Incompatible versions!": "版本不兼容!",
|
|
||||||
"Incompatible versions": "不兼容版本",
|
"Incompatible versions": "不兼容版本",
|
||||||
"Include debug logs": "包含调试日志",
|
"Include debug logs": "包含调试日志",
|
||||||
"Home": "主页",
|
"Home": "主页",
|
||||||
"Grid layout menu": "网格布局菜单",
|
|
||||||
"Go": "开始",
|
"Go": "开始",
|
||||||
"Full screen": "全屏",
|
"Full screen": "全屏",
|
||||||
"Freedom": "自由模式",
|
|
||||||
"Fetching group call timed out.": "获取群组通话超时。",
|
"Fetching group call timed out.": "获取群组通话超时。",
|
||||||
"Exit full screen": "退出全屏",
|
"Exit full screen": "退出全屏",
|
||||||
"Element Call Home": "Element Call 主页",
|
"Element Call Home": "Element Call 主页",
|
||||||
"Download debug logs": "下载调试日志",
|
"Download debug logs": "下载调试日志",
|
||||||
"Display name": "显示名称",
|
"Display name": "显示名称",
|
||||||
"Developer": "开发者",
|
"Developer": "开发者",
|
||||||
"Details": "详情",
|
|
||||||
"Debug log request": "调试日志请求",
|
"Debug log request": "调试日志请求",
|
||||||
"Debug log": "调试日志",
|
"Debug log": "调试日志",
|
||||||
"Create account": "创建账户",
|
"Create account": "创建账户",
|
||||||
@@ -87,7 +74,6 @@
|
|||||||
"Copied!": "已复制!",
|
"Copied!": "已复制!",
|
||||||
"Confirm password": "确认密码",
|
"Confirm password": "确认密码",
|
||||||
"Close": "关闭",
|
"Close": "关闭",
|
||||||
"Change layout": "更改布局",
|
|
||||||
"Camera": "摄像头",
|
"Camera": "摄像头",
|
||||||
"Call type menu": "通话类型菜单",
|
"Call type menu": "通话类型菜单",
|
||||||
"Call link copied": "链接已复制",
|
"Call link copied": "链接已复制",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>現在加入通話</0><1>或</1><2>複製通話連結,稍後再加入</2>",
|
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>現在加入通話</0><1>或</1><2>複製通話連結,稍後再加入</2>",
|
||||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>建立帳號</0> 或<2>以訪客身份登入</2>",
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>建立帳號</0> 或<2>以訪客身份登入</2>",
|
||||||
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>已經有帳號?</0><1><0>登入</0> 或<2>以訪客身份登入</2></1>",
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>已經有帳號?</0><1><0>登入</0> 或<2>以訪客身份登入</2></1>",
|
||||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
|
||||||
"Expose developer settings in the settings window.": "在設定視窗中顯示開發者設定。",
|
"Expose developer settings in the settings window.": "在設定視窗中顯示開發者設定。",
|
||||||
"Developer Settings": "開發者設定",
|
"Developer Settings": "開發者設定",
|
||||||
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>送出除錯紀錄,可幫助我們修正問題。</0>",
|
"<0>Submitting debug logs will help us track down the problem.</0>": "<0>送出除錯紀錄,可幫助我們修正問題。</0>",
|
||||||
@@ -19,13 +18,9 @@
|
|||||||
"Version: {{version}}": "版本: {{version}}",
|
"Version: {{version}}": "版本: {{version}}",
|
||||||
"Username": "使用者名稱",
|
"Username": "使用者名稱",
|
||||||
"User menu": "使用者選單",
|
"User menu": "使用者選單",
|
||||||
"Unmute microphone": "取消麥克風靜音",
|
|
||||||
"Turn on camera": "開啟相機",
|
|
||||||
"Turn off camera": "關閉相機",
|
|
||||||
"This call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?",
|
"This call already exists, would you like to join?": "通話已經開始,請問您要加入嗎?",
|
||||||
"Take me Home": "帶我回主畫面",
|
"Take me Home": "帶我回主畫面",
|
||||||
"Submit feedback": "遞交回覆",
|
"Submit feedback": "遞交回覆",
|
||||||
"Stop sharing screen": "停止分享螢幕畫面",
|
|
||||||
"Spotlight": "聚焦",
|
"Spotlight": "聚焦",
|
||||||
"Speaker": "發言者",
|
"Speaker": "發言者",
|
||||||
"Sign out": "登出",
|
"Sign out": "登出",
|
||||||
@@ -50,7 +45,6 @@
|
|||||||
"Not registered yet? <2>Create an account</2>": "還沒註冊嗎?<2>建立帳號</2>",
|
"Not registered yet? <2>Create an account</2>": "還沒註冊嗎?<2>建立帳號</2>",
|
||||||
"Not now, return to home screen": "現在不行,回到首頁",
|
"Not now, return to home screen": "現在不行,回到首頁",
|
||||||
"No": "否",
|
"No": "否",
|
||||||
"Mute microphone": "麥克風靜音",
|
|
||||||
"More": "更多",
|
"More": "更多",
|
||||||
"Microphone": "麥克風",
|
"Microphone": "麥克風",
|
||||||
"Login to your account": "登入您的帳號",
|
"Login to your account": "登入您的帳號",
|
||||||
@@ -58,28 +52,21 @@
|
|||||||
"Logging in…": "登入中…",
|
"Logging in…": "登入中…",
|
||||||
"Local volume": "您的音量",
|
"Local volume": "您的音量",
|
||||||
"Loading…": "載入中…",
|
"Loading…": "載入中…",
|
||||||
"Leave": "離開",
|
|
||||||
"Join existing call?": "加入已開始的通話嗎?",
|
"Join existing call?": "加入已開始的通話嗎?",
|
||||||
"Join call now": "現在加入通話",
|
"Join call now": "現在加入通話",
|
||||||
"Join call": "加入通話",
|
"Join call": "加入通話",
|
||||||
"Invite people": "邀請夥伴",
|
|
||||||
"Invite": "邀請",
|
|
||||||
"Inspector": "稽查員",
|
"Inspector": "稽查員",
|
||||||
"Incompatible versions!": "不相容版本!",
|
|
||||||
"Incompatible versions": "不相容版本",
|
"Incompatible versions": "不相容版本",
|
||||||
"Include debug logs": "包含除錯紀錄",
|
"Include debug logs": "包含除錯紀錄",
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
"Grid layout menu": "格框式清單",
|
|
||||||
"Go": "前往",
|
"Go": "前往",
|
||||||
"Full screen": "全螢幕",
|
"Full screen": "全螢幕",
|
||||||
"Freedom": "自由",
|
|
||||||
"Fetching group call timed out.": "加入群組對話已逾時。",
|
"Fetching group call timed out.": "加入群組對話已逾時。",
|
||||||
"Exit full screen": "退出全螢幕",
|
"Exit full screen": "退出全螢幕",
|
||||||
"Element Call Home": "Element Call 首頁",
|
"Element Call Home": "Element Call 首頁",
|
||||||
"Download debug logs": "下載偵錯報告",
|
"Download debug logs": "下載偵錯報告",
|
||||||
"Display name": "顯示名稱",
|
"Display name": "顯示名稱",
|
||||||
"Developer": "開發者",
|
"Developer": "開發者",
|
||||||
"Details": "詳細說明",
|
|
||||||
"Debug log request": "請求偵錯報告",
|
"Debug log request": "請求偵錯報告",
|
||||||
"Debug log": "除錯紀錄",
|
"Debug log": "除錯紀錄",
|
||||||
"Create account": "建立帳號",
|
"Create account": "建立帳號",
|
||||||
@@ -88,7 +75,6 @@
|
|||||||
"Copied!": "已複製!",
|
"Copied!": "已複製!",
|
||||||
"Confirm password": "確認密碼",
|
"Confirm password": "確認密碼",
|
||||||
"Close": "關閉",
|
"Close": "關閉",
|
||||||
"Change layout": "變更排列",
|
|
||||||
"Camera": "相機",
|
"Camera": "相機",
|
||||||
"Call type menu": "通話類型選單",
|
"Call type menu": "通話類型選單",
|
||||||
"Call link copied": "已複製通話連結",
|
"Call link copied": "已複製通話連結",
|
||||||
@@ -122,6 +108,5 @@
|
|||||||
"Thanks!": "感謝!",
|
"Thanks!": "感謝!",
|
||||||
"You were disconnected from the call": "您已從通話斷線",
|
"You were disconnected from the call": "您已從通話斷線",
|
||||||
"Enable end-to-end encryption (password protected calls)": "啟用端到端加密(密碼保護通話)",
|
"Enable end-to-end encryption (password protected calls)": "啟用端到端加密(密碼保護通話)",
|
||||||
"End-to-end encryption isn't supported on your browser.": "您的瀏覽器不支援端到端加密。",
|
"End-to-end encryption isn't supported on your browser.": "您的瀏覽器不支援端到端加密。"
|
||||||
"Password (if none, E2EE is disabled)": "密碼(若無,就會停用端到端加密)"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { LoginPage } from "./auth/LoginPage";
|
|||||||
import { RegisterPage } from "./auth/RegisterPage";
|
import { RegisterPage } from "./auth/RegisterPage";
|
||||||
import { RoomPage } from "./room/RoomPage";
|
import { RoomPage } from "./room/RoomPage";
|
||||||
import { ClientProvider } from "./ClientContext";
|
import { ClientProvider } from "./ClientContext";
|
||||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
|
||||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||||
import { CrashView, LoadingView } from "./FullScreenView";
|
import { CrashView, LoadingView } from "./FullScreenView";
|
||||||
@@ -48,8 +47,6 @@ export default function App({ history }: AppProps) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
usePageFocusStyle();
|
|
||||||
|
|
||||||
const errorPage = <CrashView />;
|
const errorPage = <CrashView />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
position: relative;
|
|
||||||
color: var(--stopgap-color-on-solid-accent);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
pointer-events: none;
|
|
||||||
font-weight: 600;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar svg * {
|
|
||||||
fill: var(--cpd-color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar span {
|
|
||||||
padding-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border-radius: 22px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 32px;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 36px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
border-radius: 42px;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xl {
|
|
||||||
width: 90px;
|
|
||||||
height: 90px;
|
|
||||||
border-radius: 90px;
|
|
||||||
font-size: 48px;
|
|
||||||
}
|
|
||||||
@@ -14,23 +14,11 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo, CSSProperties, HTMLAttributes, FC } from "react";
|
import { useMemo, FC } from "react";
|
||||||
import classNames from "classnames";
|
import { Avatar as CompoundAvatar } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { getAvatarUrl } from "./matrix-utils";
|
import { getAvatarUrl } from "./matrix-utils";
|
||||||
import { useClient } from "./ClientContext";
|
import { useClient } from "./ClientContext";
|
||||||
import styles from "./Avatar.module.css";
|
|
||||||
|
|
||||||
const backgroundColors = [
|
|
||||||
"#5C56F5",
|
|
||||||
"#03B381",
|
|
||||||
"#368BD6",
|
|
||||||
"#AC3BA8",
|
|
||||||
"#E64F7A",
|
|
||||||
"#FF812D",
|
|
||||||
"#2DC2C5",
|
|
||||||
"#74D12C",
|
|
||||||
];
|
|
||||||
|
|
||||||
export enum Size {
|
export enum Size {
|
||||||
XS = "xs",
|
XS = "xs",
|
||||||
@@ -48,50 +36,28 @@ export const sizes = new Map([
|
|||||||
[Size.XL, 90],
|
[Size.XL, 90],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function hashStringToArrIndex(str: string, arrLength: number) {
|
interface Props {
|
||||||
let sum = 0;
|
id: string;
|
||||||
|
name: string;
|
||||||
for (let i = 0; i < str.length; i++) {
|
className?: string;
|
||||||
sum += str.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sum % arrLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
||||||
bgKey?: string;
|
|
||||||
src?: string;
|
src?: string;
|
||||||
size?: Size | number;
|
size?: Size | number;
|
||||||
className?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
fallback: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Avatar: FC<Props> = ({
|
export const Avatar: FC<Props> = ({
|
||||||
bgKey,
|
|
||||||
src,
|
|
||||||
fallback,
|
|
||||||
size = Size.MD,
|
|
||||||
className,
|
className,
|
||||||
style = {},
|
id,
|
||||||
...rest
|
name,
|
||||||
|
src,
|
||||||
|
size = Size.MD,
|
||||||
}) => {
|
}) => {
|
||||||
const { client } = useClient();
|
const { client } = useClient();
|
||||||
|
|
||||||
const [sizeClass, sizePx, sizeStyle] = useMemo(
|
const sizePx = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.values(Size).includes(size as Size)
|
Object.values(Size).includes(size as Size)
|
||||||
? [styles[size as string], sizes.get(size as Size), {}]
|
? sizes.get(size as Size)
|
||||||
: [
|
: (size as number),
|
||||||
null,
|
|
||||||
size as number,
|
|
||||||
{
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
borderRadius: size,
|
|
||||||
fontSize: Math.round((size as number) / 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[size]
|
[size]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -100,28 +66,13 @@ export const Avatar: FC<Props> = ({
|
|||||||
return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src;
|
return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src;
|
||||||
}, [client, src, sizePx]);
|
}, [client, src, sizePx]);
|
||||||
|
|
||||||
const backgroundColor = useMemo(() => {
|
|
||||||
const index = hashStringToArrIndex(
|
|
||||||
bgKey || fallback || src || "",
|
|
||||||
backgroundColors.length
|
|
||||||
);
|
|
||||||
return backgroundColors[index];
|
|
||||||
}, [bgKey, src, fallback]);
|
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/alt-text */
|
|
||||||
return (
|
return (
|
||||||
<div
|
<CompoundAvatar
|
||||||
className={classNames(styles.avatar, sizeClass, className)}
|
className={className}
|
||||||
style={{ backgroundColor, ...sizeStyle, ...style }}
|
id={id}
|
||||||
{...rest}
|
name={name}
|
||||||
>
|
size={`${sizePx}px`}
|
||||||
{resolvedSrc ? (
|
src={resolvedSrc}
|
||||||
<img src={resolvedSrc} />
|
/>
|
||||||
) : typeof fallback === "string" ? (
|
|
||||||
<span>{fallback}</span>
|
|
||||||
) : (
|
|
||||||
fallback
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2023 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { useObjectRef } from "@react-aria/utils";
|
|
||||||
import { useButton } from "@react-aria/button";
|
|
||||||
|
|
||||||
import styles from "./E2EELock.module.css";
|
|
||||||
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
|
|
||||||
import { TooltipTrigger } from "./Tooltip";
|
|
||||||
|
|
||||||
export const E2EELock = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const tooltip = useCallback(
|
|
||||||
() => t("This call is not end-to-end encrypted."),
|
|
||||||
[t]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipTrigger placement="right" tooltip={tooltip}>
|
|
||||||
<Icon />
|
|
||||||
</TooltipTrigger>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component is a bit of hack - for some reason for the TooltipTrigger to
|
|
||||||
* work, it needs to contain a component which uses the useButton hook; please
|
|
||||||
* note that for some reason this also needs to be a separate component and we
|
|
||||||
* cannot just use the useButton hook inside the E2EELock.
|
|
||||||
*/
|
|
||||||
const Icon = () => {
|
|
||||||
const buttonRef = useObjectRef<HTMLDivElement>();
|
|
||||||
const { buttonProps } = useButton({}, buttonRef);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={buttonRef} className={styles.e2eeLock} {...buttonProps}>
|
|
||||||
<LockOffIcon />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -14,27 +14,20 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HTMLAttributes, useMemo } from "react";
|
import { HTMLAttributes } from "react";
|
||||||
import classNames from "classnames";
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AvatarStack } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import styles from "./Facepile.module.css";
|
import { Avatar, Size } from "./Avatar";
|
||||||
import { Avatar, Size, sizes } from "./Avatar";
|
|
||||||
|
|
||||||
const overlapMap: Partial<Record<Size, number>> = {
|
|
||||||
[Size.XS]: 2,
|
|
||||||
[Size.SM]: 4,
|
|
||||||
[Size.MD]: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
className: string;
|
className?: string;
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
members: RoomMember[];
|
members: RoomMember[];
|
||||||
max?: number;
|
max?: number;
|
||||||
size?: Size;
|
size?: Size | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Facepile({
|
export function Facepile({
|
||||||
@@ -47,51 +40,27 @@ export function Facepile({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const _size = sizes.get(size)!;
|
const displayedMembers = members.slice(0, max);
|
||||||
const _overlap = overlapMap[size]!;
|
|
||||||
|
|
||||||
const title = useMemo(() => {
|
|
||||||
return members.reduce<string | null>(
|
|
||||||
(prev, curr) =>
|
|
||||||
prev === null
|
|
||||||
? curr.name
|
|
||||||
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
|
|
||||||
null
|
|
||||||
) as string;
|
|
||||||
}, [members, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<AvatarStack
|
||||||
className={classNames(styles.facepile, styles[size], className)}
|
title={t("{{names, list(style: short;)}}", {
|
||||||
title={title}
|
list: displayedMembers.map((m) => m.name),
|
||||||
style={{
|
})}
|
||||||
width:
|
|
||||||
Math.min(members.length, max + 1) * (_size - _overlap) + _overlap,
|
|
||||||
}}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{members.slice(0, max).map((member, i) => {
|
{displayedMembers.map((member, i) => {
|
||||||
const avatarUrl = member.getMxcAvatarUrl();
|
const avatarUrl = member.getMxcAvatarUrl();
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
key={member.userId}
|
key={i}
|
||||||
|
id={member.userId}
|
||||||
|
name={member.name}
|
||||||
size={size}
|
size={size}
|
||||||
src={avatarUrl ?? undefined}
|
src={avatarUrl ?? undefined}
|
||||||
fallback={member.name.slice(0, 1).toUpperCase()}
|
|
||||||
className={styles.avatar}
|
|
||||||
style={{ left: i * (_size - _overlap) }}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{members.length > max && (
|
</AvatarStack>
|
||||||
<Avatar
|
|
||||||
key="additional"
|
|
||||||
size={size}
|
|
||||||
fallback={`+${members.length - max}`}
|
|
||||||
className={styles.avatar}
|
|
||||||
style={{ left: max * (_size - _overlap) }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ limitations under the License.
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0 20px;
|
padding-inline: var(--inline-content-inset);
|
||||||
height: 64px;
|
height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerLogo {
|
.headerLogo {
|
||||||
@@ -66,51 +66,56 @@ limitations under the License.
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.roomHeaderInfo {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--cpd-space-4x);
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomHeaderInfo[data-size="sm"] {
|
||||||
|
grid-template-areas: "avatar name" ". participants";
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomHeaderInfo[data-size="lg"] {
|
||||||
|
grid-template-areas: "avatar name" "avatar participants";
|
||||||
|
}
|
||||||
|
|
||||||
.roomAvatar {
|
.roomAvatar {
|
||||||
position: relative;
|
align-self: flex-start;
|
||||||
display: none;
|
grid-area: avatar;
|
||||||
justify-content: center;
|
}
|
||||||
|
|
||||||
|
.nameLine {
|
||||||
|
grid-area: name;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 36px;
|
gap: var(--cpd-space-1x);
|
||||||
height: 36px;
|
|
||||||
border-radius: 36px;
|
|
||||||
background-color: #5c56f5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomAvatar > * {
|
.nameLine > h1 {
|
||||||
fill: white;
|
margin: 0;
|
||||||
stroke: white;
|
/* XXX I can't actually get this ellipsis overflow to trigger, because
|
||||||
}
|
constraint propagation in a nested flexbox layout is a massive pain */
|
||||||
|
|
||||||
.userName {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-right: 8px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 1;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.versionMismatchWarning {
|
.nameLine > svg {
|
||||||
padding-left: 15px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.versionMismatchWarning::before {
|
.participantsLine {
|
||||||
content: "";
|
grid-area: participants;
|
||||||
display: inline-block;
|
display: flex;
|
||||||
position: relative;
|
align-items: center;
|
||||||
top: 1px;
|
gap: var(--cpd-space-1-5x);
|
||||||
width: 16px;
|
font: var(--cpd-font-body-sm-medium);
|
||||||
height: 16px;
|
|
||||||
mask-image: url("./icons/AlertTriangleFilled.svg");
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
background-color: var(--cpd-color-icon-critical-primary);
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
.headerLogo,
|
.headerLogo,
|
||||||
.roomAvatar,
|
|
||||||
.leftNav.hideMobile,
|
.leftNav.hideMobile,
|
||||||
.rightNav.hideMobile {
|
.rightNav.hideMobile {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -119,8 +124,4 @@ limitations under the License.
|
|||||||
.leftNav h3 {
|
.leftNav h3 {
|
||||||
font-size: var(--font-size-subtitle);
|
font-size: var(--font-size-subtitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
|
||||||
height: 76px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import { GridLayoutMenu } from "./room/GridLayoutMenu";
|
|
||||||
import {
|
|
||||||
Header,
|
|
||||||
HeaderLogo,
|
|
||||||
LeftNav,
|
|
||||||
RightNav,
|
|
||||||
RoomHeaderInfo,
|
|
||||||
} from "./Header";
|
|
||||||
import { UserMenu } from "./UserMenu";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: "Header",
|
|
||||||
component: Header,
|
|
||||||
parameters: {
|
|
||||||
layout: "fullscreen",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HomeAnonymous = () => (
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<HeaderLogo />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<UserMenu />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const HomeNamedGuest = () => (
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<HeaderLogo />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const HomeLoggedIn = () => (
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<HeaderLogo />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<UserMenu isAuthenticated displayName="Yara" />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const LobbyNamedGuest = () => (
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<RoomHeaderInfo roomName="Q4Roadmap" />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const LobbyLoggedIn = () => (
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<RoomHeaderInfo roomName="Q4Roadmap" />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<UserMenu isAuthenticated displayName="Yara" />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const InRoomNamedGuest = () => (
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<RoomHeaderInfo roomName="Q4Roadmap" />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<GridLayoutMenu layout="freedom" />
|
|
||||||
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const InRoomLoggedIn = () => (
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<RoomHeaderInfo roomName="Q4Roadmap" />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav>
|
|
||||||
<GridLayoutMenu layout="freedom" />
|
|
||||||
<UserMenu isAuthenticated displayName="Yara" />
|
|
||||||
</RightNav>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CreateAccount = () => (
|
|
||||||
<Header>
|
|
||||||
<LeftNav>
|
|
||||||
<HeaderLogo />
|
|
||||||
</LeftNav>
|
|
||||||
<RightNav></RightNav>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
@@ -15,17 +15,18 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { HTMLAttributes, ReactNode, useCallback } from "react";
|
import { FC, HTMLAttributes, ReactNode } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Heading } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import styles from "./Header.module.css";
|
import styles from "./Header.module.css";
|
||||||
import { useModalTriggerState } from "./Modal";
|
|
||||||
import { Button } from "./button";
|
|
||||||
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
||||||
import { Subtitle } from "./typography/Typography";
|
import { Avatar, Size } from "./Avatar";
|
||||||
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
|
import { Facepile } from "./Facepile";
|
||||||
|
import { EncryptionLock } from "./room/EncryptionLock";
|
||||||
|
import { useMediaQuery } from "./useMediaQuery";
|
||||||
|
|
||||||
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -112,47 +113,52 @@ export function HeaderLogo({ className }: HeaderLogoProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoomHeaderInfo {
|
interface RoomHeaderInfoProps {
|
||||||
roomName: string;
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
encrypted: boolean;
|
||||||
|
participants: RoomMember[];
|
||||||
|
client: MatrixClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoomHeaderInfo({ roomName }: RoomHeaderInfo) {
|
export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
||||||
return (
|
id,
|
||||||
<>
|
name,
|
||||||
<Subtitle data-testid="roomHeader_roomName" fontWeight="semiBold">
|
avatarUrl,
|
||||||
{roomName}
|
encrypted,
|
||||||
</Subtitle>
|
participants,
|
||||||
</>
|
client,
|
||||||
);
|
}) => {
|
||||||
}
|
|
||||||
|
|
||||||
interface VersionMismatchWarningProps {
|
|
||||||
users: Set<string>;
|
|
||||||
room: Room;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VersionMismatchWarning({
|
|
||||||
users,
|
|
||||||
room,
|
|
||||||
}: VersionMismatchWarningProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg";
|
||||||
|
|
||||||
const onDetailsClick = useCallback(() => {
|
|
||||||
modalState.open();
|
|
||||||
}, [modalState]);
|
|
||||||
|
|
||||||
if (users.size === 0) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.versionMismatchWarning}>
|
<div className={styles.roomHeaderInfo} data-size={size}>
|
||||||
{t("Incompatible versions!")}
|
<Avatar
|
||||||
<Button variant="link" onClick={onDetailsClick}>
|
className={styles.roomAvatar}
|
||||||
{t("Details")}
|
id={id}
|
||||||
</Button>
|
name={name}
|
||||||
{modalState.isOpen && (
|
size={size === "sm" ? Size.SM : 56}
|
||||||
<IncompatibleVersionModal userIds={users} room={room} {...modalProps} />
|
src={avatarUrl ?? undefined}
|
||||||
|
/>
|
||||||
|
<div className={styles.nameLine}>
|
||||||
|
<Heading
|
||||||
|
type={size === "sm" ? "body" : "heading"}
|
||||||
|
size={size === "sm" ? "lg" : "md"}
|
||||||
|
weight="semibold"
|
||||||
|
data-testid="roomHeader_roomName"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Heading>
|
||||||
|
<EncryptionLock encrypted={encrypted} />
|
||||||
|
</div>
|
||||||
|
{participants.length > 0 && (
|
||||||
|
<div className={styles.participantsLine}>
|
||||||
|
<Facepile client={client} members={participants} size={20} />
|
||||||
|
{t("{{count, number}}", { count: participants.length })}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
|
||||||
import { FC, useMemo } from "react";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { Modal, ModalContent } from "./Modal";
|
|
||||||
import { Body } from "./typography/Typography";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
userIds: Set<string>;
|
|
||||||
room: Room;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const IncompatibleVersionModal: FC<Props> = ({
|
|
||||||
userIds,
|
|
||||||
room,
|
|
||||||
onClose,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const userLis = useMemo(
|
|
||||||
() => [...userIds].map((u) => <li>{room.getMember(u)?.name ?? u}</li>),
|
|
||||||
[userIds, room]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={t("Incompatible versions")}
|
|
||||||
isDismissable
|
|
||||||
onClose={onClose}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<ModalContent>
|
|
||||||
<Body>
|
|
||||||
<Trans>
|
|
||||||
Other users are trying to join this call from incompatible versions.
|
|
||||||
These users should ensure that they have refreshed their browsers:
|
|
||||||
<ul>{userLis}</ul>
|
|
||||||
</Trans>
|
|
||||||
</Body>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -24,17 +24,3 @@ limitations under the License.
|
|||||||
.userButton svg * {
|
.userButton svg * {
|
||||||
fill: var(--cpd-color-icon-primary);
|
fill: var(--cpd-color-icon-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
font-size: var(--font-size-caption);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.avatar {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
font-size: var(--font-size-body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ interface UserMenuProps {
|
|||||||
preventNavigation: boolean;
|
preventNavigation: boolean;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isPasswordlessUser: boolean;
|
isPasswordlessUser: boolean;
|
||||||
|
userId: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
onAction: (value: string) => void;
|
onAction: (value: string) => void;
|
||||||
@@ -44,6 +45,7 @@ export function UserMenu({
|
|||||||
preventNavigation,
|
preventNavigation,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
|
userId,
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
onAction,
|
onAction,
|
||||||
@@ -109,10 +111,10 @@ export function UserMenu({
|
|||||||
>
|
>
|
||||||
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
id={userId}
|
||||||
|
name={displayName}
|
||||||
size={Size.SM}
|
size={Size.SM}
|
||||||
className={styles.avatar}
|
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
|||||||
isPasswordlessUser={passwordlessUser}
|
isPasswordlessUser={passwordlessUser}
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
onAction={onAction}
|
onAction={onAction}
|
||||||
|
userId={client?.getUserId() ?? ""}
|
||||||
displayName={displayName || (userName ? userName.replace("@", "") : "")}
|
displayName={displayName || (userName ? userName.replace("@", "") : "")}
|
||||||
/>
|
/>
|
||||||
{modalState.isOpen && client && (
|
{modalState.isOpen && client && (
|
||||||
|
|||||||
@@ -50,14 +50,14 @@ limitations under the License.
|
|||||||
background-color: var(--cpd-color-text-action-accent);
|
background-color: var(--cpd-color-text-action-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:focus,
|
.button:focus-visible,
|
||||||
.toolbarButton:focus,
|
.toolbarButton:focus-visible,
|
||||||
.toolbarButtonSecondary:focus,
|
.toolbarButtonSecondary:focus-visible,
|
||||||
.iconButton:focus,
|
.iconButton:focus-visible,
|
||||||
.iconCopyButton:focus,
|
.iconCopyButton:focus-visible,
|
||||||
.secondary:focus,
|
.secondary:focus-visible,
|
||||||
.secondaryHangup:focus,
|
.secondaryHangup:focus-visible,
|
||||||
.copyButton:focus {
|
.copyButton:focus-visible {
|
||||||
outline: auto;
|
outline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ limitations under the License.
|
|||||||
background-color: var(--cpd-color-bg-canvas-default);
|
background-color: var(--cpd-color-bg-canvas-default);
|
||||||
color: var(--cpd-color-icon-primary);
|
color: var(--cpd-color-icon-primary);
|
||||||
border: 1px solid var(--cpd-color-gray-400);
|
border: 1px solid var(--cpd-color-gray-400);
|
||||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
box-shadow: var(--subtle-drop-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbarButton.on,
|
.toolbarButton.on,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { forwardRef, useCallback } from "react";
|
import { forwardRef } from "react";
|
||||||
import { PressEvent } from "@react-types/shared";
|
import { PressEvent } from "@react-types/shared";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useButton } from "@react-aria/button";
|
import { useButton } from "@react-aria/button";
|
||||||
@@ -27,13 +27,11 @@ import { ReactComponent as VideoCallOffIcon } from "@vector-im/compound-design-t
|
|||||||
import { ReactComponent as EndCallIcon } from "@vector-im/compound-design-tokens/icons/end-call.svg";
|
import { ReactComponent as EndCallIcon } from "@vector-im/compound-design-tokens/icons/end-call.svg";
|
||||||
import { ReactComponent as ShareScreenSolidIcon } from "@vector-im/compound-design-tokens/icons/share-screen-solid.svg";
|
import { ReactComponent as ShareScreenSolidIcon } from "@vector-im/compound-design-tokens/icons/share-screen-solid.svg";
|
||||||
import { ReactComponent as SettingsSolidIcon } from "@vector-im/compound-design-tokens/icons/settings-solid.svg";
|
import { ReactComponent as SettingsSolidIcon } from "@vector-im/compound-design-tokens/icons/settings-solid.svg";
|
||||||
import { ReactComponent as UserAddSolidIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg";
|
|
||||||
import { ReactComponent as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg";
|
import { ReactComponent as ChevronDownIcon } from "@vector-im/compound-design-tokens/icons/chevron-down.svg";
|
||||||
|
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg";
|
import { ReactComponent as Fullscreen } from "../icons/Fullscreen.svg";
|
||||||
import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg";
|
import { ReactComponent as FullscreenExit } from "../icons/FullscreenExit.svg";
|
||||||
import { TooltipTrigger } from "../Tooltip";
|
|
||||||
import { VolumeIcon } from "./VolumeIcon";
|
import { VolumeIcon } from "./VolumeIcon";
|
||||||
|
|
||||||
export type ButtonVariant =
|
export type ButtonVariant =
|
||||||
@@ -146,11 +144,13 @@ export function MicButton({
|
|||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon;
|
||||||
|
const label = muted ? t("Microphone off") : t("Microphone on");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={muted ? t("Microphone off") : t("Microphone on")}>
|
<Tooltip label={label}>
|
||||||
<Button variant="toolbar" {...rest} on={!muted}>
|
<Button variant="toolbar" {...rest} on={!muted}>
|
||||||
{muted ? <MicOffSolidIcon /> : <MicOnSolidIcon />}
|
<Icon aria-label={label} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -165,11 +165,13 @@ export function VideoButton({
|
|||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const Icon = muted ? VideoCallOffIcon : VideoCallIcon;
|
||||||
|
const label = muted ? t("Video off") : t("Video on");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={muted ? t("Video off") : t("Video on")}>
|
<Tooltip label={label}>
|
||||||
<Button variant="toolbar" {...rest} on={!muted}>
|
<Button variant="toolbar" {...rest} on={!muted}>
|
||||||
{muted ? <VideoCallOffIcon /> : <VideoCallIcon />}
|
<Icon aria-label={label} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -186,11 +188,12 @@ export function ScreenshareButton({
|
|||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const label = enabled ? t("Sharing screen") : t("Share screen");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={enabled ? t("Sharing screen") : t("Share screen")}>
|
<Tooltip label={label}>
|
||||||
<Button variant="toolbar" {...rest} on={enabled}>
|
<Button variant="toolbar" {...rest} on={enabled}>
|
||||||
<ShareScreenSolidIcon />
|
<ShareScreenSolidIcon aria-label={label} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -213,7 +216,7 @@ export function HangupButton({
|
|||||||
className={classNames(styles.hangupButton, className)}
|
className={classNames(styles.hangupButton, className)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<EndCallIcon />
|
<EndCallIcon aria-label={t("End call")} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -232,28 +235,7 @@ export function SettingsButton({
|
|||||||
return (
|
return (
|
||||||
<Tooltip label={t("Settings")}>
|
<Tooltip label={t("Settings")}>
|
||||||
<Button variant="toolbar" {...rest}>
|
<Button variant="toolbar" {...rest}>
|
||||||
<SettingsSolidIcon />
|
<SettingsSolidIcon aria-label={t("Settings")} />
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InviteButton({
|
|
||||||
className,
|
|
||||||
variant = "toolbar",
|
|
||||||
...rest
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
variant?: string;
|
|
||||||
// TODO: add all props for <Button>
|
|
||||||
[index: string]: unknown;
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip label={t("Invite")}>
|
|
||||||
<Button variant={variant} {...rest}>
|
|
||||||
<UserAddSolidIcon />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -268,14 +250,13 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
|
|||||||
|
|
||||||
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const tooltip = useCallback(() => t("Local volume"), [t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger tooltip={tooltip}>
|
<Tooltip label={t("Local volume")}>
|
||||||
<Button variant="icon" {...rest}>
|
<Button variant="icon" {...rest}>
|
||||||
<VolumeIcon volume={volume} />
|
<VolumeIcon volume={volume} aria-label={t("Local volume")} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,15 +269,14 @@ export function FullscreenButton({
|
|||||||
...rest
|
...rest
|
||||||
}: FullscreenButtonProps) {
|
}: FullscreenButtonProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const tooltip = useCallback(() => {
|
const Icon = fullscreen ? FullscreenExit : Fullscreen;
|
||||||
return fullscreen ? t("Exit full screen") : t("Full screen");
|
const label = fullscreen ? t("Exit full screen") : t("Full screen");
|
||||||
}, [fullscreen, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger tooltip={tooltip}>
|
<Tooltip label={label}>
|
||||||
<Button variant="icon" {...rest}>
|
<Button variant="icon" {...rest}>
|
||||||
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
|
<Icon aria-label={label} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/button/ShareButton.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ComponentPropsWithoutRef, FC } from "react";
|
||||||
|
import { Button } from "@vector-im/compound-web";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ReactComponent as UserAddSolidIcon } from "@vector-im/compound-design-tokens/icons/user-add-solid.svg";
|
||||||
|
|
||||||
|
export const ShareButton: FC<
|
||||||
|
Omit<ComponentPropsWithoutRef<"button">, "children">
|
||||||
|
> = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Button kind="secondary" size="sm" Icon={UserAddSolidIcon} {...props}>
|
||||||
|
{t("Share")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,19 +15,21 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ComponentPropsWithoutRef, FC } from "react";
|
||||||
|
|
||||||
import { ReactComponent as AudioMuted } from "../icons/AudioMuted.svg";
|
import { ReactComponent as AudioMuted } from "../icons/AudioMuted.svg";
|
||||||
import { ReactComponent as AudioLow } from "../icons/AudioLow.svg";
|
import { ReactComponent as AudioLow } from "../icons/AudioLow.svg";
|
||||||
import { ReactComponent as Audio } from "../icons/Audio.svg";
|
import { ReactComponent as Audio } from "../icons/Audio.svg";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends ComponentPropsWithoutRef<"svg"> {
|
||||||
/**
|
/**
|
||||||
* Number between 0 and 1
|
* Number between 0 and 1
|
||||||
*/
|
*/
|
||||||
volume: number;
|
volume: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VolumeIcon({ volume }: Props) {
|
export const VolumeIcon: FC<Props> = ({ volume, ...rest }) => {
|
||||||
if (volume <= 0) return <AudioMuted />;
|
if (volume <= 0) return <AudioMuted {...rest} />;
|
||||||
if (volume <= 0.5) return <AudioLow />;
|
if (volume <= 0.5) return <AudioLow {...rest} />;
|
||||||
return <Audio />;
|
return <Audio {...rest} />;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
|||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
import { Facepile } from "../Facepile";
|
|
||||||
import { Avatar, Size } from "../Avatar";
|
import { Avatar, Size } from "../Avatar";
|
||||||
import styles from "./CallList.module.css";
|
import styles from "./CallList.module.css";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
@@ -30,9 +29,8 @@ import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
|||||||
interface CallListProps {
|
interface CallListProps {
|
||||||
rooms: GroupCallRoom[];
|
rooms: GroupCallRoom[];
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
disableFacepile?: boolean;
|
|
||||||
}
|
}
|
||||||
export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
export function CallList({ rooms, client }: CallListProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.callList}>
|
<div className={styles.callList}>
|
||||||
@@ -44,7 +42,6 @@ export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
|||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
roomId={room.roomId}
|
roomId={room.roomId}
|
||||||
participants={participants}
|
participants={participants}
|
||||||
disableFacepile={disableFacepile}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{rooms.length > 3 && (
|
{rooms.length > 3 && (
|
||||||
@@ -63,39 +60,18 @@ interface CallTileProps {
|
|||||||
roomId: string;
|
roomId: string;
|
||||||
participants: RoomMember[];
|
participants: RoomMember[];
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
disableFacepile?: boolean;
|
|
||||||
}
|
}
|
||||||
function CallTile({
|
function CallTile({ name, avatarUrl, roomId }: CallTileProps) {
|
||||||
name,
|
|
||||||
avatarUrl,
|
|
||||||
roomId,
|
|
||||||
participants,
|
|
||||||
client,
|
|
||||||
disableFacepile,
|
|
||||||
}: CallTileProps) {
|
|
||||||
const roomSharedKey = useRoomSharedKey(roomId);
|
const roomSharedKey = useRoomSharedKey(roomId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.callTile}>
|
<div className={styles.callTile}>
|
||||||
<Link to={`/room/#?roomId=${roomId}`} className={styles.callTileLink}>
|
<Link to={`/room/#?roomId=${roomId}`} className={styles.callTileLink}>
|
||||||
<Avatar
|
<Avatar id={roomId} name={name} size={Size.LG} src={avatarUrl} />
|
||||||
size={Size.LG}
|
|
||||||
bgKey={name}
|
|
||||||
src={avatarUrl}
|
|
||||||
fallback={name.slice(0, 1).toUpperCase()}
|
|
||||||
className={styles.avatar}
|
|
||||||
/>
|
|
||||||
<div className={styles.callInfo}>
|
<div className={styles.callInfo}>
|
||||||
<Body overflowEllipsis fontWeight="semiBold">
|
<Body overflowEllipsis fontWeight="semiBold">
|
||||||
{name}
|
{name}
|
||||||
</Body>
|
</Body>
|
||||||
{participants && !disableFacepile && (
|
|
||||||
<Facepile
|
|
||||||
className={styles.facePile}
|
|
||||||
client={client}
|
|
||||||
members={participants}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.copyButtonSpacer} />
|
<div className={styles.copyButtonSpacer} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
|||||||
<Title className={styles.recentCallsTitle}>
|
<Title className={styles.recentCallsTitle}>
|
||||||
{t("Your recent calls")}
|
{t("Your recent calls")}
|
||||||
</Title>
|
</Title>
|
||||||
<CallList rooms={recentRooms} client={client} disableFacepile />
|
<CallList rooms={recentRooms} client={client} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M18.8914 9.0784C18.5132 9.0784 18.2066 8.777 18.2066 8.4052V5.71251H15.4678C15.0896 5.71251 14.783 5.41111 14.783 5.03931C14.783 4.66751 15.0896 4.36611 15.4678 4.36611H18.2066V1.6732C18.2066 1.3014 18.5132 1 18.8914 1C19.2696 1 19.5761 1.3014 19.5761 1.6732V4.36611H22.3153C22.6934 4.36611 23 4.66751 23 5.03931C23 5.41111 22.6934 5.71251 22.3153 5.71251H19.5761V8.4052C19.5761 8.777 19.2696 9.0784 18.8914 9.0784Z" fill="#ffffff"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5002 11.5094C19.8949 11.5094 20.281 11.4736 20.6555 11.4051C20.7505 11.9558 20.8 12.5221 20.8 13.1C20.8 18.5676 16.3676 23 10.9 23C5.43238 23 1 18.5676 1 13.1C1 7.63238 5.43238 3.2 10.9 3.2C11.7973 3.2 12.6666 3.31937 13.4931 3.54308C13.3177 4.11118 13.2234 4.71396 13.2234 5.33841C13.2234 8.74655 16.0336 11.5094 19.5002 11.5094ZM11.1308 21.4653C13.2927 21.4653 15.2551 20.6258 16.6993 19.2603C15.7939 17.0769 13.6417 15.5412 11.1308 15.5412C8.61988 15.5412 6.46768 17.0769 5.56225 19.2603C7.00643 20.6258 8.96888 21.4653 11.1308 21.4653ZM11.2799 7.81768C9.78854 7.9515 8.62035 9.18434 8.62035 10.6854C8.62035 12.2759 9.93179 13.5652 11.5495 13.5652C12.6276 13.5652 13.5696 12.9927 14.0781 12.1401C12.6655 11.1035 11.6462 9.57751 11.2799 7.81768Z" fill="#ffffff"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2.47012 18H17.5301C19.0701 18 20.0301 16.33 19.2601 15L11.7301 1.98999C10.9601 0.659993 9.04012 0.659993 8.27012 1.98999L0.740121 15C-0.0298788 16.33 0.930121 18 2.47012 18ZM10.0001 11C9.45012 11 9.00012 10.55 9.00012 9.99999V7.99999C9.00012 7.44999 9.45012 6.99999 10.0001 6.99999C10.5501 6.99999 11.0001 7.44999 11.0001 7.99999V9.99999C11.0001 10.55 10.5501 11 10.0001 11ZM11.0001 15H9.00012V13H11.0001V15Z" fill="#737D8C"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 540 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M18.6663 8H1.33301M1.33301 8L7.83301 14.5M1.33301 8L7.83301 1.5" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 258 B |
@@ -1,6 +0,0 @@
|
|||||||
<svg data-testid="icon_videomute" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0.290472 1.37627C0.677768 0.985743 1.3057 0.985743 1.69299 1.37627L16.569 16.3762C16.9563 16.7668 16.9563 17.3999 16.569 17.7904C16.1817 18.181 15.5538 18.181 15.1665 17.7904L0.290472 2.79048C-0.096824 2.39995 -0.096824 1.76679 0.290472 1.37627Z" fill="#394049"/>
|
|
||||||
<path d="M0.597515 5.19186C0.323238 5.646 0.165249 6.17941 0.165249 6.75001V14.0833C0.165249 15.7402 1.49729 17.0833 3.14045 17.0833H12.363L0.639137 5.2371C0.624608 5.22242 0.610733 5.20733 0.597515 5.19186Z" fill="#394049"/>
|
|
||||||
<path d="M14.2148 6.75002V11.9031L6.14598 3.75002H11.2396C12.8828 3.75002 14.2148 5.09317 14.2148 6.75002Z" fill="#394049"/>
|
|
||||||
<path d="M18.3887 5.88312L15.8677 7.91669V12.9167L18.3887 14.9503C19.038 15.4741 19.9999 15.0079 19.9999 14.1694V6.66399C19.9999 5.82548 19.038 5.35931 18.3887 5.88312Z" fill="#394049"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 944 B |
@@ -1,6 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="13" y="11" width="5" height="5" rx="1.16667" fill="white"/>
|
|
||||||
<rect x="13" y="5" width="5" height="5" rx="1.16667" fill="white"/>
|
|
||||||
<rect x="7" y="5" width="5" height="5" rx="1.16667" fill="white"/>
|
|
||||||
<rect x="4" y="11" width="8" height="8" rx="1.14286" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 375 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M6.68883 13.3012C7.63969 14.3278 9.92871 16.1054 10.5526 16.4702C10.5895 16.4917 10.6317 16.5168 10.679 16.5449C11.631 17.1109 14.6141 18.8842 16.7875 17.2246C18.4714 15.9389 17.9239 14.4926 17.3571 14.0626C16.9691 13.7608 15.8258 12.9288 14.7502 12.1795C13.694 11.4436 13.1053 12.0332 12.7073 12.4318C12.7 12.4391 12.6927 12.4464 12.6856 12.4536L11.8848 13.2544C11.6808 13.4583 11.3706 13.3839 11.0735 13.1505C10.0074 12.3385 9.22308 11.555 8.8307 11.1626L8.82739 11.1593C8.43506 10.767 7.66123 9.99241 6.84932 8.92627C6.61592 8.62914 6.54149 8.31893 6.74542 8.11501L7.54622 7.31421C7.55341 7.30702 7.56067 7.29977 7.568 7.29245C7.96663 6.89444 8.55618 6.30581 7.82033 5.24958C7.07097 4.17394 6.239 3.03068 5.93717 2.64265C5.50723 2.07588 4.06088 1.52836 2.77514 3.21225C1.11556 5.38572 2.88891 8.36878 3.45484 9.32078C3.48293 9.36803 3.50805 9.41028 3.52962 9.44717C3.8944 10.0711 5.66224 12.3503 6.68883 13.3012Z" fill="white"/>
|
|
||||||
<path d="M17.2473 2.56504C17.4497 2.3595 17.4497 2.02626 17.2473 1.82072C17.0448 1.61518 16.7166 1.61518 16.5142 1.82072L14.1663 4.20452L11.8185 1.82072C11.6161 1.61518 11.2878 1.61518 11.0854 1.82072C10.883 2.02626 10.883 2.3595 11.0854 2.56504L13.4332 4.94884L10.9848 7.43475C10.7824 7.64029 10.7824 7.97354 10.9848 8.17908C11.1873 8.38462 11.5155 8.38462 11.7179 8.17908L14.1663 5.69316L16.6148 8.17907C16.8172 8.38461 17.1454 8.38461 17.3478 8.17907C17.5503 7.97354 17.5503 7.64029 17.3478 7.43475L14.8994 4.94884L17.2473 2.56504Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
11
src/icons/LogoMark.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Logo Mark">
|
||||||
|
<rect width="47.86" height="48" rx="23.93" fill="#0DBD8B"/>
|
||||||
|
<g id="Union">
|
||||||
|
<path d="M21.3075 9.42871C20.3396 9.42871 19.5549 10.214 19.5549 11.1828C19.5549 12.1516 20.3396 12.9369 21.3075 12.9369C25.9321 12.9369 29.6811 16.689 29.6811 21.3175C29.6811 22.2863 30.4657 23.0716 31.4337 23.0716C32.4016 23.0716 33.1863 22.2863 33.1863 21.3175C33.1863 14.7515 27.868 9.42871 21.3075 9.42871Z" fill="white"/>
|
||||||
|
<path d="M38.4591 21.3174C38.4591 20.3486 37.6745 19.5633 36.7065 19.5633C35.7386 19.5633 34.9539 20.3486 34.9539 21.3174C34.9539 25.9459 31.2049 29.698 26.5804 29.698C25.6124 29.698 24.8277 30.4833 24.8277 31.4521C24.8277 32.4209 25.6124 33.2062 26.5804 33.2062C33.1408 33.2062 38.4591 27.8834 38.4591 21.3174Z" fill="white"/>
|
||||||
|
<path d="M28.3329 36.8173C28.3329 37.786 27.5482 38.5714 26.5803 38.5714C20.0198 38.5714 14.7015 33.2486 14.7015 26.6826C14.7015 25.7138 15.4862 24.9285 16.4541 24.9285C17.4221 24.9285 18.2067 25.7138 18.2067 26.6826C18.2067 31.3111 21.9557 35.0632 26.5803 35.0632C27.5482 35.0632 28.3329 35.8485 28.3329 36.8173Z" fill="white"/>
|
||||||
|
<path d="M9.40112 26.6827C9.40112 27.6514 10.1858 28.4368 11.1537 28.4368C12.1217 28.4368 12.9064 27.6514 12.9064 26.6827C12.9064 22.0542 16.6553 18.3021 21.2799 18.3021C22.2478 18.3021 23.0325 17.5167 23.0325 16.548C23.0325 15.5792 22.2478 14.7939 21.2799 14.7939C14.7194 14.7939 9.40112 20.1167 9.40112 26.6827Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
17
src/icons/LogoType.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg width="160" height="22" viewBox="0 0 160 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Logo Type">
|
||||||
|
<g id="Vector">
|
||||||
|
<path d="M14.8673 15.1575H3.39742C3.53293 16.3508 3.96849 17.3036 4.70411 18.0157C5.43974 18.7087 6.40766 19.0551 7.60789 19.0551C8.40159 19.0551 9.11785 18.8626 9.75668 18.4777C10.3955 18.0927 10.8504 17.5731 11.1215 16.9186H14.606C14.1414 18.4392 13.2702 19.671 11.9926 20.6142C10.7343 21.5381 9.24368 22 7.52078 22C5.27519 22 3.45549 21.259 2.06168 19.7769C0.687227 18.2948 0 16.4182 0 14.147C0 11.9335 0.696906 10.0761 2.09072 8.5748C3.48453 7.07349 5.28487 6.32283 7.49174 6.32283C9.69861 6.32283 11.4796 7.06387 12.8347 8.54593C14.2091 10.0087 14.8964 11.8565 14.8964 14.0892L14.8673 15.1575ZM7.49174 9.12336C6.40766 9.12336 5.50749 9.44095 4.79123 10.0761C4.07496 10.7113 3.62972 11.5582 3.45549 12.6168H11.4699C11.315 11.5582 10.8892 10.7113 10.1922 10.0761C9.49534 9.44095 8.59517 9.12336 7.49174 9.12336Z" fill="currentColor"/>
|
||||||
|
<path d="M17.2743 17.1785V0H20.7298V17.2362C20.7298 18.0061 21.1557 18.3911 22.0074 18.3911L22.6172 18.3622V21.6247C22.2881 21.6824 21.9397 21.7113 21.5719 21.7113C20.0813 21.7113 18.9875 21.336 18.2906 20.5853C17.6131 19.8346 17.2743 18.699 17.2743 17.1785Z" fill="currentColor"/>
|
||||||
|
<path d="M38.71 15.1575H27.2401C27.3756 16.3508 27.8112 17.3036 28.5468 18.0157C29.2824 18.7087 30.2504 19.0551 31.4506 19.0551C32.2443 19.0551 32.9606 18.8626 33.5994 18.4777C34.2382 18.0927 34.6931 17.5731 34.9642 16.9186H38.4487C37.9841 18.4392 37.113 19.671 35.8353 20.6142C34.577 21.5381 33.0864 22 31.3635 22C29.1179 22 27.2982 21.259 25.9044 19.7769C24.5299 18.2948 23.8427 16.4182 23.8427 14.147C23.8427 11.9335 24.5396 10.0761 25.9334 8.5748C27.3272 7.07349 29.1276 6.32283 31.3344 6.32283C33.5413 6.32283 35.3223 7.06387 36.6774 8.54593C38.0518 10.0087 38.7391 11.8565 38.7391 14.0892L38.71 15.1575ZM31.3344 9.12336C30.2504 9.12336 29.3502 9.44095 28.6339 10.0761C27.9177 10.7113 27.4724 11.5582 27.2982 12.6168H35.3126C35.1577 11.5582 34.7319 10.7113 34.035 10.0761C33.3381 9.44095 32.4379 9.12336 31.3344 9.12336Z" fill="currentColor"/>
|
||||||
|
<path d="M54.3001 13.0499V21.6535H50.8446V12.6745C50.8446 10.4033 49.8961 9.26772 47.9989 9.26772C46.9729 9.26772 46.1502 9.59493 45.5307 10.2493C44.9306 10.9038 44.6306 11.7988 44.6306 12.9344V21.6535H41.1751V6.66929H44.3692V8.66142C44.737 7.98775 45.2984 7.42957 46.0534 6.98688C46.8084 6.54418 47.7473 6.32283 48.8701 6.32283C50.9608 6.32283 52.4707 7.11199 53.4 8.69029C54.6776 7.11199 56.3812 6.32283 58.5106 6.32283C60.2722 6.32283 61.6273 6.87139 62.5759 7.9685C63.5244 9.04637 63.9987 10.4707 63.9987 12.2415V21.6535H60.5432V12.6745C60.5432 10.4033 59.5947 9.26772 57.6975 9.26772C56.6522 9.26772 55.8198 9.60455 55.2003 10.2782C54.6002 10.9326 54.3001 11.8565 54.3001 13.0499Z" fill="currentColor"/>
|
||||||
|
<path d="M81.1834 15.1575H69.7135C69.849 16.3508 70.2846 17.3036 71.0202 18.0157C71.7558 18.7087 72.7237 19.0551 73.924 19.0551C74.7177 19.0551 75.4339 18.8626 76.0728 18.4777C76.7116 18.0927 77.1665 17.5731 77.4375 16.9186H80.9221C80.4575 18.4392 79.5863 19.671 78.3087 20.6142C77.0504 21.5381 75.5598 22 73.8369 22C71.5913 22 69.7716 21.259 68.3778 19.7769C67.0033 18.2948 66.3161 16.4182 66.3161 14.147C66.3161 11.9335 67.013 10.0761 68.4068 8.5748C69.8006 7.07349 71.601 6.32283 73.8078 6.32283C76.0147 6.32283 77.7957 7.06387 79.1508 8.54593C80.5252 10.0087 81.2124 11.8565 81.2124 14.0892L81.1834 15.1575ZM73.8078 9.12336C72.7237 9.12336 71.8236 9.44095 71.1073 10.0761C70.391 10.7113 69.9458 11.5582 69.7716 12.6168H77.786C77.6311 11.5582 77.2052 10.7113 76.5083 10.0761C75.8114 9.44095 74.9113 9.12336 73.8078 9.12336Z" fill="currentColor"/>
|
||||||
|
<path d="M86.8426 6.66929V8.66142C87.191 8.007 87.7621 7.45844 88.5558 7.01575C89.3689 6.55381 90.3465 6.32283 91.4886 6.32283C93.2696 6.32283 94.6441 6.86177 95.612 7.93963C96.5993 9.0175 97.0929 10.4514 97.0929 12.2415V21.6535H93.6374V12.6745C93.6374 11.6159 93.3858 10.7883 92.8824 10.1916C92.3985 9.57568 91.6532 9.26772 90.6465 9.26772C89.5431 9.26772 88.672 9.59493 88.0331 10.2493C87.4137 10.9038 87.1039 11.8084 87.1039 12.9633V21.6535H83.6484V6.66929H86.8426Z" fill="currentColor"/>
|
||||||
|
<path d="M107.185 18.5932V21.5669C106.759 21.6824 106.159 21.7402 105.384 21.7402C102.442 21.7402 100.971 20.2677 100.971 17.3228V9.41208H98.6766V6.66929H100.971V2.77165H104.426V6.66929H107.243V9.41208H104.426V16.9764C104.426 18.1505 104.987 18.7375 106.11 18.7375L107.185 18.5932Z" fill="currentColor"/>
|
||||||
|
<path d="M116.115 18.9881C114.474 17.2035 113.653 14.9429 113.653 12.2064C113.653 9.4699 114.474 7.21782 116.115 5.45015C117.773 3.66548 119.953 2.77314 122.654 2.77314C124.876 2.77314 126.756 3.38503 128.295 4.6088C129.833 5.83258 130.816 7.47277 131.244 9.52939H129.269C128.91 7.99967 128.132 6.7844 126.936 5.88357C125.739 4.98273 124.312 4.53232 122.654 4.53232C120.534 4.53232 118.824 5.23769 117.525 6.64842C116.243 8.05916 115.602 9.91182 115.602 12.2064C115.602 14.501 116.243 16.3536 117.525 17.7644C118.824 19.1751 120.534 19.8805 122.654 19.8805C124.312 19.8805 125.739 19.4301 126.936 18.5292C128.132 17.6284 128.91 16.4131 129.269 14.8834H131.244C130.816 16.94 129.833 18.5802 128.295 19.804C126.756 21.0278 124.876 21.6397 122.654 21.6397C119.953 21.6397 117.773 20.7558 116.115 18.9881Z" fill="currentColor"/>
|
||||||
|
<path d="M143.174 15.0874C140.832 15.0874 139.233 15.1384 138.379 15.2403C137.541 15.3253 136.926 15.4698 136.532 15.6738C135.831 16.0647 135.481 16.6936 135.481 17.5604C135.481 19.2261 136.473 20.0589 138.456 20.0589C139.977 20.0589 141.139 19.719 141.943 19.0391C142.763 18.3593 143.174 17.4499 143.174 16.3111V15.0874ZM138.25 21.5632C136.763 21.5632 135.626 21.2062 134.84 20.4924C134.071 19.7615 133.686 18.8012 133.686 17.6114C133.686 16.8295 133.891 16.1327 134.301 15.5208C134.729 14.9089 135.31 14.4585 136.045 14.1695C136.661 13.9316 137.455 13.7786 138.43 13.7106C139.404 13.6256 140.986 13.5831 143.174 13.5831V12.7418C143.174 10.6002 141.943 9.52939 139.481 9.52939C137.361 9.52939 136.131 10.3877 135.789 12.1044H134.019C134.207 10.8466 134.746 9.84383 135.635 9.09597C136.541 8.34811 137.849 7.97418 139.558 7.97418C141.387 7.97418 142.746 8.3991 143.635 9.24894C144.541 10.0988 144.994 11.2716 144.994 12.7673V21.2572H143.251V19.3706C142.345 20.8323 140.678 21.5632 138.25 21.5632Z" fill="currentColor"/>
|
||||||
|
<path d="M149.358 18.4018V2.13576H151.178V18.1978C151.178 18.7247 151.264 19.0901 151.435 19.2941C151.623 19.498 151.956 19.6 152.435 19.6L152.948 19.549V21.2062C152.657 21.2572 152.341 21.2827 151.999 21.2827C150.238 21.2827 149.358 20.3224 149.358 18.4018Z" fill="currentColor"/>
|
||||||
|
<path d="M155.944 18.4018V2.13576H157.764V18.1978C157.764 18.7247 157.85 19.0901 158.021 19.2941C158.209 19.498 158.542 19.6 159.021 19.6L159.534 19.549V21.2062C159.243 21.2572 158.927 21.2827 158.585 21.2827C156.824 21.2827 155.944 20.3224 155.944 18.4018Z" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="24" height="17" viewBox="0 0 24 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 0.5C2.09315 0.5 0.75 1.84314 0.75 3.5V13.5713C0.75 15.2282 2.09315 16.5713 3.75 16.5713H20.2499C21.9067 16.5713 23.2499 15.2282 23.2499 13.5713V3.5C23.2499 1.84315 21.9067 0.5 20.2499 0.5H3.75ZM12.7998 11.9106C12.7998 12.3524 12.4416 12.7106 11.9998 12.7106C11.558 12.7106 11.1998 12.3524 11.1998 11.9106L11.1998 7.09206L9.43158 8.86029C9.11916 9.17271 8.61263 9.17271 8.30021 8.86029C7.98779 8.54787 7.98779 8.04134 8.30021 7.72892L11.4341 4.59501C11.7465 4.28259 12.2531 4.28259 12.5655 4.59501L15.6994 7.72892C16.0118 8.04134 16.0118 8.54787 15.6994 8.86029C15.387 9.17271 14.8805 9.17271 14.568 8.86029L12.7998 7.09206L12.7998 11.9106Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 814 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 17.8689V2.51551C4 2.06669 4.53728 1.83452 4.86986 2.13591C8.18767 5.14263 10.9111 7.48209 13.102 9.36399L13.102 9.36403C18.3295 13.8544 20.5243 15.7398 20.5243 17.8689C20.5243 19.4181 19.6538 20.0153 18.1044 20.79C16.5549 21.5648 14.4534 22 12.2621 22C10.0709 22 7.96938 21.5648 6.41992 20.79C4.87047 20.0153 4 18.9646 4 17.8689ZM12.2621 20.9673C16.2548 20.9673 19.4915 19.5801 19.4915 17.869C19.4915 16.1578 16.2548 14.7707 12.2621 14.7707C8.26947 14.7707 5.03277 16.1578 5.03277 17.869C5.03277 19.5801 8.26947 20.9673 12.2621 20.9673ZM16.2618 8.67876C16.1718 8.64549 16.1718 8.51831 16.2618 8.48504L17.84 7.90103C17.8683 7.89057 17.8906 7.86828 17.901 7.84001L18.4851 6.26174C18.5183 6.17182 18.6455 6.17182 18.6788 6.26174L19.2628 7.84001C19.2733 7.86828 19.2955 7.89057 19.3238 7.90103L20.9021 8.48504C20.992 8.51831 20.992 8.64549 20.9021 8.67876L19.3238 9.26277C19.2955 9.27323 19.2733 9.29552 19.2628 9.32379L18.6788 10.9021C18.6455 10.992 18.5183 10.992 18.4851 10.9021L17.901 9.32379C17.8906 9.29552 17.8683 9.27323 17.84 9.26277L16.2618 8.67876ZM13.2618 5.45232C13.1718 5.48559 13.1718 5.61276 13.2618 5.64604L14.0862 5.95111C14.1145 5.96157 14.1368 5.98386 14.1472 6.01213L14.4523 6.83657C14.4856 6.92649 14.6127 6.92649 14.646 6.83657L14.9511 6.01213C14.9615 5.98386 14.9838 5.96157 15.0121 5.95111L15.8365 5.64603C15.9265 5.61276 15.9265 5.48559 15.8365 5.45232L15.0121 5.14725C14.9838 5.13679 14.9615 5.1145 14.9511 5.08623L14.646 4.26178C14.6127 4.17187 14.4856 4.17187 14.4523 4.26178L14.1472 5.08623C14.1368 5.1145 14.1145 5.13679 14.0862 5.14725L13.2618 5.45232Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<svg data-testid="videoTile_muted" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0.20333 0.963373C0.474437 0.690007 0.913989 0.690007 1.1851 0.963373L11.5983 11.4633C11.8694 11.7367 11.8694 12.1799 11.5983 12.4533C11.3272 12.7267 10.8876 12.7267 10.6165 12.4533L0.20333 1.95332C-0.0677768 1.67995 -0.0677768 1.23674 0.20333 0.963373Z" fill="white"/>
|
|
||||||
<path d="M0.418261 3.63429C0.226267 3.95219 0.115674 4.32557 0.115674 4.725V9.85832C0.115674 11.0181 1.0481 11.9583 2.19831 11.9583H8.65411L0.447396 3.66596C0.437225 3.65568 0.427513 3.64511 0.418261 3.63429Z" fill="white"/>
|
|
||||||
<path d="M9.95036 4.725V8.33212L4.30219 2.625H7.86772C9.01793 2.625 9.95036 3.5652 9.95036 4.725Z" fill="white"/>
|
|
||||||
<path d="M12.8721 4.11817L11.1074 5.54167V9.04166L12.8721 10.4652C13.3266 10.8318 14 10.5055 14 9.91855V4.66478C14 4.07782 13.3266 3.7515 12.8721 4.11817Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 922 B |
@@ -39,15 +39,22 @@ limitations under the License.
|
|||||||
--font-size-title: calc(24px * var(--font-scale));
|
--font-size-title: calc(24px * var(--font-scale));
|
||||||
--font-size-headline: calc(32px * var(--font-scale));
|
--font-size-headline: calc(32px * var(--font-scale));
|
||||||
|
|
||||||
--cpd-color-border-accent: var(--cpd-color-green-1100);
|
|
||||||
/* These colors are needed during the transitionary period between the old and
|
/* These colors are needed during the transitionary period between the old and
|
||||||
new Compound design systems, but should be removed ASAP */
|
new Compound design systems, but should be removed ASAP */
|
||||||
--stopgap-color-on-solid-accent: var(--cpd-color-bg-canvas-default);
|
--stopgap-color-on-solid-accent: var(--cpd-color-bg-canvas-default);
|
||||||
--stopgap-background-85: rgba(255, 255, 255, 0.85);
|
--stopgap-background-85: rgba(255, 255, 255, 0.85);
|
||||||
--stopgap-bgColor3: #444;
|
--stopgap-bgColor3: #444;
|
||||||
|
|
||||||
|
--cpd-color-border-accent: var(--cpd-color-green-800);
|
||||||
|
/* The distance to inset non-full-width content from the edge of the window
|
||||||
|
along the inline axis */
|
||||||
|
--inline-content-inset: max(var(--cpd-space-4x), calc((100vw - 1180px) / 2));
|
||||||
|
--small-drop-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
|
||||||
|
--subtle-drop-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpd-theme-dark {
|
.cpd-theme-dark {
|
||||||
|
--cpd-color-border-accent: var(--cpd-color-green-1100);
|
||||||
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
|
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
|
||||||
--stopgap-background-85: rgba(16, 19, 23, 0.85);
|
--stopgap-background-85: rgba(16, 19, 23, 0.85);
|
||||||
}
|
}
|
||||||
@@ -240,3 +247,13 @@ details[open] > summary {
|
|||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* normalize.css sets the focus rings on buttons in Firefox to an unusual custom
|
||||||
|
outline, which is inconsistent with our other components and is not sufficiently
|
||||||
|
visible to be accessible. This resets it back to 'auto'. */
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,13 +35,23 @@ interface Props extends AllHTMLAttributes<HTMLInputElement> {
|
|||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
avatarUrl: string | undefined;
|
avatarUrl: string | undefined;
|
||||||
|
userId: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
onRemoveAvatar: () => void;
|
onRemoveAvatar: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
||||||
(
|
(
|
||||||
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
|
{
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
avatarUrl,
|
||||||
|
userId,
|
||||||
|
displayName,
|
||||||
|
onRemoveAvatar,
|
||||||
|
...rest
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -80,9 +90,10 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
|||||||
<div className={classNames(styles.avatarInputField, className)}>
|
<div className={classNames(styles.avatarInputField, className)}>
|
||||||
<div className={styles.avatarContainer}>
|
<div className={styles.avatarContainer}>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
id={userId}
|
||||||
|
name={displayName}
|
||||||
size={Size.XL}
|
size={Size.XL}
|
||||||
src={removed ? undefined : objUrl || avatarUrl}
|
src={removed ? undefined : objUrl || avatarUrl}
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.e2eeLock {
|
import { Focus } from "matrix-js-sdk/src/matrixrtc/focus";
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
|
|
||||||
display: flex;
|
export interface LivekitFocus extends Focus {
|
||||||
align-items: center;
|
type: "livekit";
|
||||||
justify-content: center;
|
livekit_service_url: string;
|
||||||
margin: 8px;
|
livekit_alias: string;
|
||||||
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: var(--cpd-color-bg-subtle-primary);
|
|
||||||
}
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2023 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
ReactNode,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { GroupCall } from "matrix-js-sdk";
|
|
||||||
|
|
||||||
import {
|
|
||||||
OpenIDClientParts,
|
|
||||||
SFUConfig,
|
|
||||||
getSFUConfigWithOpenID,
|
|
||||||
} from "./openIDSFU";
|
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
client: OpenIDClientParts;
|
|
||||||
groupCall: GroupCall;
|
|
||||||
roomName: string;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SFUConfigContext = createContext<SFUConfig | undefined>(undefined);
|
|
||||||
|
|
||||||
export const useSFUConfig = () => useContext(SFUConfigContext);
|
|
||||||
|
|
||||||
export function OpenIDLoader({ client, groupCall, roomName, children }: Props) {
|
|
||||||
const [state, setState] = useState<
|
|
||||||
SFUConfigLoading | SFUConfigLoaded | SFUConfigFailed
|
|
||||||
>({ kind: "loading" });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const result = await getSFUConfigWithOpenID(
|
|
||||||
client,
|
|
||||||
groupCall,
|
|
||||||
roomName
|
|
||||||
);
|
|
||||||
setState({ kind: "loaded", sfuConfig: result });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to fetch SFU config: ", e);
|
|
||||||
setState({ kind: "failed", error: e as Error });
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [client, groupCall, roomName]);
|
|
||||||
|
|
||||||
switch (state.kind) {
|
|
||||||
case "loading":
|
|
||||||
return <LoadingView />;
|
|
||||||
case "failed":
|
|
||||||
return <ErrorView error={state.error} />;
|
|
||||||
case "loaded":
|
|
||||||
return (
|
|
||||||
<SFUConfigContext.Provider value={state.sfuConfig}>
|
|
||||||
{children}
|
|
||||||
</SFUConfigContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SFUConfigLoading = {
|
|
||||||
kind: "loading";
|
|
||||||
};
|
|
||||||
|
|
||||||
type SFUConfigLoaded = {
|
|
||||||
kind: "loaded";
|
|
||||||
sfuConfig: SFUConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SFUConfigFailed = {
|
|
||||||
kind: "failed";
|
|
||||||
error: Error;
|
|
||||||
};
|
|
||||||
@@ -14,82 +14,78 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GroupCall, IOpenIDToken, MatrixClient } from "matrix-js-sdk";
|
import { IOpenIDToken, MatrixClient } from "matrix-js-sdk";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Config } from "../config/Config";
|
import { LivekitFocus } from "./LivekitFocus";
|
||||||
|
import { useActiveFocus } from "../room/useActiveFocus";
|
||||||
|
|
||||||
export interface SFUConfig {
|
export interface SFUConfig {
|
||||||
url: string;
|
url: string;
|
||||||
jwt: string;
|
jwt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sfuConfigEquals(a?: SFUConfig, b?: SFUConfig): boolean {
|
||||||
|
if (a === undefined && b === undefined) return true;
|
||||||
|
if (a === undefined || b === undefined) return false;
|
||||||
|
|
||||||
|
return a.jwt === b.jwt && a.url === b.url;
|
||||||
|
}
|
||||||
|
|
||||||
// The bits we need from MatrixClient
|
// The bits we need from MatrixClient
|
||||||
export type OpenIDClientParts = Pick<
|
export type OpenIDClientParts = Pick<
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
"getOpenIdToken" | "getDeviceId"
|
"getOpenIdToken" | "getDeviceId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export function useOpenIDSFU(
|
||||||
|
client: OpenIDClientParts,
|
||||||
|
rtcSession: MatrixRTCSession
|
||||||
|
) {
|
||||||
|
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
|
||||||
|
|
||||||
|
const activeFocus = useActiveFocus(rtcSession);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const sfuConfig = activeFocus
|
||||||
|
? await getSFUConfigWithOpenID(client, activeFocus)
|
||||||
|
: undefined;
|
||||||
|
setSFUConfig(sfuConfig);
|
||||||
|
})();
|
||||||
|
}, [client, activeFocus]);
|
||||||
|
|
||||||
|
return sfuConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSFUConfigWithOpenID(
|
export async function getSFUConfigWithOpenID(
|
||||||
client: OpenIDClientParts,
|
client: OpenIDClientParts,
|
||||||
groupCall: GroupCall,
|
activeFocus: LivekitFocus
|
||||||
roomName: string
|
): Promise<SFUConfig | undefined> {
|
||||||
): Promise<SFUConfig> {
|
|
||||||
const openIdToken = await client.getOpenIdToken();
|
const openIdToken = await client.getOpenIdToken();
|
||||||
logger.debug("Got openID token", openIdToken);
|
logger.debug("Got openID token", openIdToken);
|
||||||
|
|
||||||
// if the call has a livekit service URL, try it.
|
|
||||||
if (groupCall.livekitServiceURL) {
|
|
||||||
try {
|
|
||||||
logger.info(
|
|
||||||
`Trying to get JWT from call's configured URL of ${groupCall.livekitServiceURL}...`
|
|
||||||
);
|
|
||||||
const sfuConfig = await getLiveKitJWT(
|
|
||||||
client,
|
|
||||||
groupCall.livekitServiceURL,
|
|
||||||
roomName,
|
|
||||||
openIdToken
|
|
||||||
);
|
|
||||||
logger.info(`Got JWT from call state event URL.`);
|
|
||||||
|
|
||||||
return sfuConfig;
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(
|
|
||||||
`Failed to get JWT from group call's configured URL of ${groupCall.livekitServiceURL}.`,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, try our configured one and, if it works, update the call's service URL in the state event
|
|
||||||
// NB. This wuill update it for everyone so we may end up with multiple clients updating this when they
|
|
||||||
// join at similar times, but we don't have a huge number of options here.
|
|
||||||
const urlFromConf = Config.get().livekit!.livekit_service_url;
|
|
||||||
logger.info(`Trying livekit service URL from our config: ${urlFromConf}...`);
|
|
||||||
try {
|
try {
|
||||||
|
logger.info(
|
||||||
|
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`
|
||||||
|
);
|
||||||
const sfuConfig = await getLiveKitJWT(
|
const sfuConfig = await getLiveKitJWT(
|
||||||
client,
|
client,
|
||||||
urlFromConf,
|
activeFocus.livekit_service_url,
|
||||||
roomName,
|
activeFocus.livekit_alias,
|
||||||
openIdToken
|
openIdToken
|
||||||
);
|
);
|
||||||
|
logger.info(`Got JWT from call's active focus URL.`);
|
||||||
logger.info(
|
|
||||||
`Got JWT, updating call livekit service URL with: ${urlFromConf}...`
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await groupCall.updateLivekitServiceURL(urlFromConf);
|
|
||||||
logger.info(`Call livekit service URL updated.`);
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(
|
|
||||||
`Failed to update call livekit service URL: continuing anyway.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sfuConfig;
|
return sfuConfig;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Failed to get JWT from URL defined in Config.", e);
|
logger.warn(
|
||||||
throw e;
|
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
src/livekit/useECConnectionState.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ConnectionState, Room, RoomEvent } from "livekit-client";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { SFUConfig, sfuConfigEquals } from "./openIDSFU";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Additional values for states that a call can be in, beyond what livekit
|
||||||
|
* provides in ConnectionState. Also reconnects the call if the SFU Config
|
||||||
|
* changes.
|
||||||
|
*/
|
||||||
|
export enum ECAddonConnectionState {
|
||||||
|
// We are switching from one focus to another (or between livekit room aliases on the same focus)
|
||||||
|
ECSwitchingFocus = "ec_switching_focus",
|
||||||
|
// The call has just been initialised and is waiting for credentials to arrive before attempting
|
||||||
|
// to connect. This distinguishes from the 'Disconected' state which is now just for when livekit
|
||||||
|
// gives up on connectivity and we consider the call to have failed.
|
||||||
|
ECWaiting = "ec_waiting",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ECConnectionState = ConnectionState | ECAddonConnectionState;
|
||||||
|
|
||||||
|
// This is mostly necessary because an empty useRef is an empty object
|
||||||
|
// which is truthy, so we can't just use Boolean(currentSFUConfig.current)
|
||||||
|
function sfuConfigValid(sfuConfig?: SFUConfig): boolean {
|
||||||
|
return Boolean(sfuConfig?.url) && Boolean(sfuConfig?.jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useECConnectionState(
|
||||||
|
livekitRoom?: Room,
|
||||||
|
sfuConfig?: SFUConfig
|
||||||
|
): ECConnectionState {
|
||||||
|
const [connState, setConnState] = useState(
|
||||||
|
sfuConfig && livekitRoom
|
||||||
|
? livekitRoom.state
|
||||||
|
: ECAddonConnectionState.ECWaiting
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isSwitchingFocus, setSwitchingFocus] = useState(false);
|
||||||
|
|
||||||
|
const onConnStateChanged = useCallback((state: ConnectionState) => {
|
||||||
|
if (state == ConnectionState.Connected) setSwitchingFocus(false);
|
||||||
|
setConnState(state);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const oldRoom = livekitRoom;
|
||||||
|
|
||||||
|
if (livekitRoom) {
|
||||||
|
livekitRoom.on(RoomEvent.ConnectionStateChanged, onConnStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (oldRoom)
|
||||||
|
oldRoom.off(RoomEvent.ConnectionStateChanged, onConnStateChanged);
|
||||||
|
};
|
||||||
|
}, [livekitRoom, onConnStateChanged]);
|
||||||
|
|
||||||
|
const currentSFUConfig = useRef(Object.assign({}, sfuConfig));
|
||||||
|
|
||||||
|
// Id we are transitioning from a valid config to another valid one, we need
|
||||||
|
// to explicitly switch focus
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
sfuConfigValid(sfuConfig) &&
|
||||||
|
sfuConfigValid(currentSFUConfig.current) &&
|
||||||
|
!sfuConfigEquals(currentSFUConfig.current, sfuConfig)
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`
|
||||||
|
);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
setSwitchingFocus(true);
|
||||||
|
await livekitRoom?.disconnect();
|
||||||
|
await livekitRoom?.connect(sfuConfig!.url, sfuConfig!.jwt);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSFUConfig.current = Object.assign({}, sfuConfig);
|
||||||
|
}, [sfuConfig, livekitRoom]);
|
||||||
|
|
||||||
|
return isSwitchingFocus ? ECAddonConnectionState.ECSwitchingFocus : connState;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
RoomOptions,
|
RoomOptions,
|
||||||
setLogLevel,
|
setLogLevel,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { useConnectionState, useLiveKitRoom } from "@livekit/components-react";
|
import { useLiveKitRoom } from "@livekit/components-react";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
@@ -35,6 +35,10 @@ import {
|
|||||||
MediaDevices,
|
MediaDevices,
|
||||||
useMediaDevices,
|
useMediaDevices,
|
||||||
} from "./MediaDevicesContext";
|
} from "./MediaDevicesContext";
|
||||||
|
import {
|
||||||
|
ECConnectionState,
|
||||||
|
useECConnectionState,
|
||||||
|
} from "./useECConnectionState";
|
||||||
|
|
||||||
export type E2EEConfig = {
|
export type E2EEConfig = {
|
||||||
sharedKey: string;
|
sharedKey: string;
|
||||||
@@ -42,11 +46,16 @@ export type E2EEConfig = {
|
|||||||
|
|
||||||
setLogLevel("debug");
|
setLogLevel("debug");
|
||||||
|
|
||||||
|
interface UseLivekitResult {
|
||||||
|
livekitRoom?: Room;
|
||||||
|
connState: ECConnectionState;
|
||||||
|
}
|
||||||
|
|
||||||
export function useLiveKit(
|
export function useLiveKit(
|
||||||
muteStates: MuteStates,
|
muteStates: MuteStates,
|
||||||
sfuConfig?: SFUConfig,
|
sfuConfig?: SFUConfig,
|
||||||
e2eeConfig?: E2EEConfig
|
e2eeConfig?: E2EEConfig
|
||||||
): Room | undefined {
|
): UseLivekitResult {
|
||||||
const e2eeOptions = useMemo(() => {
|
const e2eeOptions = useMemo(() => {
|
||||||
if (!e2eeConfig?.sharedKey) return undefined;
|
if (!e2eeConfig?.sharedKey) return undefined;
|
||||||
|
|
||||||
@@ -101,7 +110,7 @@ export function useLiveKit(
|
|||||||
room: roomWithoutProps,
|
room: roomWithoutProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connectionState = useConnectionState(roomWithoutProps);
|
const connectionState = useECConnectionState(room, sfuConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Sync the requested mute states with LiveKit's mute states. We do it this
|
// Sync the requested mute states with LiveKit's mute states. We do it this
|
||||||
@@ -149,5 +158,8 @@ export function useLiveKit(
|
|||||||
}
|
}
|
||||||
}, [room, devices, connectionState]);
|
}, [room, devices, connectionState]);
|
||||||
|
|
||||||
return room;
|
return {
|
||||||
|
connState: connectionState,
|
||||||
|
livekitRoom: room,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ export async function initClient(
|
|||||||
localTimeoutMs: 5000,
|
localTimeoutMs: 5000,
|
||||||
useE2eForGroupCall: e2eEnabled,
|
useE2eForGroupCall: e2eEnabled,
|
||||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||||
useLivekitForGroupCalls: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2022 New Vector Ltd
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@@ -14,29 +14,21 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.facepile {
|
.lock {
|
||||||
width: 100%;
|
padding: var(--cpd-space-1x);
|
||||||
position: relative;
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
.facepile.xs {
|
.lock[data-encrypted="true"] {
|
||||||
height: 24px;
|
color: var(--cpd-color-icon-success-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.facepile.sm {
|
.lock[data-encrypted="false"] {
|
||||||
height: 32px;
|
color: var(--cpd-color-icon-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.facepile.md {
|
@media (hover: hover) {
|
||||||
height: 36px;
|
.lock:hover {
|
||||||
}
|
background: var(--cpd-color-bg-subtle-primary);
|
||||||
|
}
|
||||||
.facepile .avatar {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
border: 1px solid var(--cpd-color-bg-canvas-default);
|
|
||||||
}
|
|
||||||
|
|
||||||
.facepile.md .avatar {
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
}
|
||||||
46
src/room/EncryptionLock.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ReactComponent as LockIcon } from "@vector-im/compound-design-tokens/icons/lock.svg";
|
||||||
|
import { ReactComponent as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg";
|
||||||
|
|
||||||
|
import styles from "./EncryptionLock.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
encrypted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EncryptionLock: FC<Props> = ({ encrypted }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const Icon = encrypted ? LockIcon : LockOffIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={encrypted ? t("Encrypted") : t("Not encrypted")}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className={styles.lock}
|
||||||
|
data-encrypted={encrypted}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { Item } from "@react-stately/collections";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { Button } from "../button";
|
|
||||||
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
|
||||||
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg";
|
|
||||||
import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
|
|
||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
|
||||||
import menuStyles from "../Menu.module.css";
|
|
||||||
import { Menu } from "../Menu";
|
|
||||||
import { TooltipTrigger } from "../Tooltip";
|
|
||||||
|
|
||||||
export type Layout = "freedom" | "spotlight";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
layout: Layout;
|
|
||||||
setLayout: (layout: Layout) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GridLayoutMenu({ layout, setLayout }: Props) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const tooltip = useCallback(() => t("Change layout"), [t]);
|
|
||||||
|
|
||||||
const onAction = useCallback(
|
|
||||||
(key: React.Key) => {
|
|
||||||
setLayout(key.toString() as Layout);
|
|
||||||
},
|
|
||||||
[setLayout]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClose = useCallback(() => {}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PopoverMenuTrigger placement="bottom right">
|
|
||||||
<TooltipTrigger tooltip={tooltip}>
|
|
||||||
<Button variant="icon">
|
|
||||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
{(props: JSX.IntrinsicAttributes) => (
|
|
||||||
<Menu
|
|
||||||
{...props}
|
|
||||||
label={t("Grid layout menu")}
|
|
||||||
onAction={onAction}
|
|
||||||
onClose={onClose}
|
|
||||||
>
|
|
||||||
<Item key="freedom" textValue={t("Freedom")}>
|
|
||||||
<FreedomIcon />
|
|
||||||
<span>Freedom</span>
|
|
||||||
{layout === "freedom" && (
|
|
||||||
<CheckIcon className={menuStyles.checkIcon} />
|
|
||||||
)}
|
|
||||||
</Item>
|
|
||||||
<Item key="spotlight" textValue={t("Spotlight")}>
|
|
||||||
<SpotlightIcon />
|
|
||||||
<span>Spotlight</span>
|
|
||||||
{layout === "spotlight" && (
|
|
||||||
<CheckIcon className={menuStyles.checkIcon} />
|
|
||||||
)}
|
|
||||||
</Item>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
</PopoverMenuTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,8 @@ limitations under the License.
|
|||||||
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
@@ -26,7 +26,7 @@ interface Props {
|
|||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
viaServers: string[];
|
viaServers: string[];
|
||||||
children: (groupCall: GroupCall) => ReactNode;
|
children: (rtcSession: MatrixRTCSession) => ReactNode;
|
||||||
createPtt: boolean;
|
createPtt: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export function GroupCallLoader({
|
|||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
case "loaded":
|
case "loaded":
|
||||||
return <>{children(groupCallState.groupCall)}</>;
|
return <>{children(groupCallState.rtcSession)}</>;
|
||||||
case "failed":
|
case "failed":
|
||||||
return <ErrorView error={groupCallState.error} />;
|
return <ErrorView error={groupCallState.error} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,37 +16,42 @@ limitations under the License.
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Room } from "livekit-client";
|
import { Room } from "livekit-client";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
import { JoinRule, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
||||||
import { useGroupCall } from "./useGroupCall";
|
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
import { MatrixInfo } from "./VideoPreview";
|
import { MatrixInfo } from "./VideoPreview";
|
||||||
import { CallEndedView } from "./CallEndedView";
|
import { CallEndedView } from "./CallEndedView";
|
||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||||
import { useProfile } from "../profile/useProfile";
|
import { useProfile } from "../profile/useProfile";
|
||||||
import { findDeviceByName } from "../media-utils";
|
import { findDeviceByName } from "../media-utils";
|
||||||
import { OpenIDLoader } from "../livekit/OpenIDLoader";
|
|
||||||
import { ActiveCall } from "./InCallView";
|
import { ActiveCall } from "./InCallView";
|
||||||
import { Config } from "../config/Config";
|
|
||||||
import { MuteStates, useMuteStates } from "./MuteStates";
|
import { MuteStates, useMuteStates } from "./MuteStates";
|
||||||
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
|
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
|
||||||
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
|
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
|
||||||
|
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
|
||||||
import {
|
import {
|
||||||
useManageRoomSharedKey,
|
useManageRoomSharedKey,
|
||||||
useIsRoomE2EE,
|
useIsRoomE2EE,
|
||||||
} from "../e2ee/sharedKeyManagement";
|
} from "../e2ee/sharedKeyManagement";
|
||||||
import { useEnableE2EE } from "../settings/useSetting";
|
import { useEnableE2EE } from "../settings/useSetting";
|
||||||
|
import { useRoomAvatar } from "./useRoomAvatar";
|
||||||
|
import { useRoomName } from "./useRoomName";
|
||||||
|
import { useModalTriggerState } from "../Modal";
|
||||||
|
import { useJoinRule } from "./useJoinRule";
|
||||||
|
import { ShareModal } from "./ShareModal";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
groupCall?: GroupCall;
|
rtcSession?: MatrixRTCSession;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +61,7 @@ interface Props {
|
|||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
preload: boolean;
|
preload: boolean;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
groupCall: GroupCall;
|
rtcSession: MatrixRTCSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCallView({
|
export function GroupCallView({
|
||||||
@@ -65,46 +70,67 @@ export function GroupCallView({
|
|||||||
isEmbedded,
|
isEmbedded,
|
||||||
preload,
|
preload,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
groupCall,
|
rtcSession,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const {
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
state,
|
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
||||||
error,
|
|
||||||
enter,
|
|
||||||
leave,
|
|
||||||
participants,
|
|
||||||
unencryptedEventsFromUsers,
|
|
||||||
otelGroupCallMembership,
|
|
||||||
} = useGroupCall(groupCall, client);
|
|
||||||
|
|
||||||
const e2eeSharedKey = useManageRoomSharedKey(groupCall.room.roomId);
|
const e2eeSharedKey = useManageRoomSharedKey(rtcSession.room.roomId);
|
||||||
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
|
const isRoomE2EE = useIsRoomE2EE(rtcSession.room.roomId);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.groupCall = groupCall;
|
window.rtcSession = rtcSession;
|
||||||
return () => {
|
return () => {
|
||||||
delete window.groupCall;
|
delete window.rtcSession;
|
||||||
};
|
};
|
||||||
}, [groupCall]);
|
}, [rtcSession]);
|
||||||
|
|
||||||
const { displayName, avatarUrl } = useProfile(client);
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
|
const roomName = useRoomName(rtcSession.room);
|
||||||
|
const roomAvatar = useRoomAvatar(rtcSession.room);
|
||||||
|
const roomEncrypted = useIsRoomE2EE(rtcSession.room.roomId)!;
|
||||||
|
|
||||||
const matrixInfo = useMemo((): MatrixInfo => {
|
const matrixInfo = useMemo((): MatrixInfo => {
|
||||||
return {
|
return {
|
||||||
|
userId: client.getUserId()!,
|
||||||
displayName: displayName!,
|
displayName: displayName!,
|
||||||
avatarUrl: avatarUrl!,
|
avatarUrl: avatarUrl!,
|
||||||
roomId: groupCall.room.roomId,
|
roomId: rtcSession.room.roomId,
|
||||||
roomName: groupCall.room.name,
|
roomName,
|
||||||
roomAlias: groupCall.room.getCanonicalAlias(),
|
roomAlias: rtcSession.room.getCanonicalAlias(),
|
||||||
|
roomAvatar,
|
||||||
|
roomEncrypted,
|
||||||
};
|
};
|
||||||
}, [displayName, avatarUrl, groupCall]);
|
}, [
|
||||||
|
displayName,
|
||||||
|
avatarUrl,
|
||||||
|
rtcSession,
|
||||||
|
roomName,
|
||||||
|
roomAvatar,
|
||||||
|
roomEncrypted,
|
||||||
|
client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const participatingMembers = useMemo(() => {
|
||||||
|
const members: RoomMember[] = [];
|
||||||
|
// Count each member only once, regardless of how many devices they use
|
||||||
|
const addedUserIds = new Set<string>();
|
||||||
|
for (const membership of memberships) {
|
||||||
|
if (!addedUserIds.has(membership.member.userId)) {
|
||||||
|
addedUserIds.add(membership.member.userId);
|
||||||
|
members.push(membership.member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
}, [memberships]);
|
||||||
|
|
||||||
const deviceContext = useMediaDevices();
|
const deviceContext = useMediaDevices();
|
||||||
const latestDevices = useRef<MediaDevices>();
|
const latestDevices = useRef<MediaDevices>();
|
||||||
latestDevices.current = deviceContext;
|
latestDevices.current = deviceContext;
|
||||||
|
|
||||||
const muteStates = useMuteStates(participants.size);
|
const muteStates = useMuteStates(memberships.length);
|
||||||
const latestMuteStates = useRef<MuteStates>();
|
const latestMuteStates = useRef<MuteStates>();
|
||||||
latestMuteStates.current = muteStates;
|
latestMuteStates.current = muteStates;
|
||||||
|
|
||||||
@@ -161,10 +187,13 @@ export function GroupCallView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await enter();
|
enterRTCSession(rtcSession);
|
||||||
|
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
// we only have room sessions right now, so call ID is the emprty string - we use the room ID
|
||||||
|
PosthogAnalytics.instance.eventCallStarted.track(
|
||||||
|
rtcSession.room.roomId
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
widget!.api.setAlwaysOnScreen(true),
|
widget!.api.setAlwaysOnScreen(true),
|
||||||
@@ -177,19 +206,18 @@ export function GroupCallView({
|
|||||||
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [groupCall, preload, enter]);
|
}, [rtcSession, preload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEmbedded && !preload) {
|
if (isEmbedded && !preload) {
|
||||||
// In embedded mode, bypass the lobby and just enter the call straight away
|
// In embedded mode, bypass the lobby and just enter the call straight away
|
||||||
enter();
|
enterRTCSession(rtcSession);
|
||||||
|
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
// use the room ID as above
|
||||||
|
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
||||||
}
|
}
|
||||||
}, [groupCall, isEmbedded, preload, enter]);
|
}, [rtcSession, isEmbedded, preload]);
|
||||||
|
|
||||||
useSentryGroupCallHandler(groupCall);
|
|
||||||
|
|
||||||
const [left, setLeft] = useState(false);
|
const [left, setLeft] = useState(false);
|
||||||
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
|
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
|
||||||
@@ -200,21 +228,16 @@ export function GroupCallView({
|
|||||||
setLeaveError(leaveError);
|
setLeaveError(leaveError);
|
||||||
setLeft(true);
|
setLeft(true);
|
||||||
|
|
||||||
let participantCount = 0;
|
|
||||||
for (const deviceMap of groupCall.participants.values()) {
|
|
||||||
participantCount += deviceMap.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
|
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
|
||||||
// therefore we want the event to be sent instantly without getting queued/batched.
|
// therefore we want the event to be sent instantly without getting queued/batched.
|
||||||
const sendInstantly = !!widget;
|
const sendInstantly = !!widget;
|
||||||
PosthogAnalytics.instance.eventCallEnded.track(
|
PosthogAnalytics.instance.eventCallEnded.track(
|
||||||
groupCall.groupCallId,
|
rtcSession.room.roomId,
|
||||||
participantCount,
|
rtcSession.memberships.length,
|
||||||
sendInstantly
|
sendInstantly
|
||||||
);
|
);
|
||||||
|
|
||||||
leave();
|
leaveRTCSession(rtcSession);
|
||||||
if (widget) {
|
if (widget) {
|
||||||
// we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked.
|
// we need to wait until the callEnded event is tracked. Otherwise the iFrame gets killed before the callEnded event got tracked.
|
||||||
await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms
|
await new Promise((resolve) => window.setTimeout(resolve, 10)); // 10ms
|
||||||
@@ -231,13 +254,13 @@ export function GroupCallView({
|
|||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[groupCall, leave, isPasswordlessUser, isEmbedded, history]
|
[rtcSession, isPasswordlessUser, isEmbedded, history]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget && state === GroupCallState.Entered) {
|
if (widget && isJoined) {
|
||||||
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
leave();
|
leaveRTCSession(rtcSession);
|
||||||
await widget!.api.transport.reply(ev.detail, {});
|
await widget!.api.transport.reply(ev.detail, {});
|
||||||
widget!.api.setAlwaysOnScreen(false);
|
widget!.api.setAlwaysOnScreen(false);
|
||||||
};
|
};
|
||||||
@@ -246,7 +269,7 @@ export function GroupCallView({
|
|||||||
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [groupCall, state, leave]);
|
}, [isJoined, rtcSession]);
|
||||||
|
|
||||||
const [e2eeEnabled] = useEnableE2EE();
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
|
|
||||||
@@ -258,8 +281,19 @@ export function GroupCallView({
|
|||||||
const onReconnect = useCallback(() => {
|
const onReconnect = useCallback(() => {
|
||||||
setLeft(false);
|
setLeft(false);
|
||||||
setLeaveError(undefined);
|
setLeaveError(undefined);
|
||||||
groupCall.enter();
|
enterRTCSession(rtcSession);
|
||||||
}, [groupCall]);
|
}, [rtcSession]);
|
||||||
|
|
||||||
|
const joinRule = useJoinRule(rtcSession.room);
|
||||||
|
|
||||||
|
const { modalState: shareModalState, modalProps: shareModalProps } =
|
||||||
|
useModalTriggerState();
|
||||||
|
|
||||||
|
const onShareClickFn = useCallback(
|
||||||
|
() => shareModalState.open(),
|
||||||
|
[shareModalState]
|
||||||
|
);
|
||||||
|
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
|
||||||
|
|
||||||
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) {
|
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) {
|
||||||
return (
|
return (
|
||||||
@@ -277,33 +311,27 @@ export function GroupCallView({
|
|||||||
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
|
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const livekitServiceURL =
|
const shareModal = shareModalState.isOpen && (
|
||||||
groupCall.livekitServiceURL ?? Config.get().livekit?.livekit_service_url;
|
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} />
|
||||||
if (!livekitServiceURL) {
|
);
|
||||||
return <ErrorView error={new Error("No livekit_service_url defined")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
if (isJoined) {
|
||||||
return <ErrorView error={error} />;
|
|
||||||
} else if (state === GroupCallState.Entered) {
|
|
||||||
return (
|
return (
|
||||||
<OpenIDLoader
|
<>
|
||||||
client={client}
|
{shareModal}
|
||||||
groupCall={groupCall}
|
|
||||||
roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
|
|
||||||
>
|
|
||||||
<ActiveCall
|
<ActiveCall
|
||||||
client={client}
|
client={client}
|
||||||
groupCall={groupCall}
|
matrixInfo={matrixInfo}
|
||||||
participants={participants}
|
rtcSession={rtcSession}
|
||||||
|
participatingMembers={participatingMembers}
|
||||||
onLeave={onLeave}
|
onLeave={onLeave}
|
||||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
e2eeConfig={e2eeConfig}
|
e2eeConfig={e2eeConfig}
|
||||||
otelGroupCallMembership={otelGroupCallMembership}
|
//otelGroupCallMembership={otelGroupCallMembership}
|
||||||
|
onShareClick={onShareClick}
|
||||||
/>
|
/>
|
||||||
</OpenIDLoader>
|
</>
|
||||||
);
|
);
|
||||||
} else if (left) {
|
} else if (left) {
|
||||||
// The call ended view is shown for two reasons: prompting guests to create
|
// The call ended view is shown for two reasons: prompting guests to create
|
||||||
@@ -319,7 +347,7 @@ export function GroupCallView({
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<CallEndedView
|
<CallEndedView
|
||||||
endedCallId={groupCall.groupCallId}
|
endedCallId={rtcSession.room.roomId}
|
||||||
client={client}
|
client={client}
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
leaveError={leaveError}
|
leaveError={leaveError}
|
||||||
@@ -342,13 +370,19 @@ export function GroupCallView({
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<LobbyView
|
<>
|
||||||
matrixInfo={matrixInfo}
|
{shareModal}
|
||||||
muteStates={muteStates}
|
<LobbyView
|
||||||
onEnter={() => enter()}
|
client={client}
|
||||||
isEmbedded={isEmbedded}
|
matrixInfo={matrixInfo}
|
||||||
hideHeader={hideHeader}
|
muteStates={muteStates}
|
||||||
/>
|
onEnter={() => enterRTCSession(rtcSession)}
|
||||||
|
isEmbedded={isEmbedded}
|
||||||
|
hideHeader={hideHeader}
|
||||||
|
participatingMembers={participatingMembers}
|
||||||
|
onShareClick={onShareClick}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,30 +49,38 @@ limitations under the License.
|
|||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
box-sizing: border-box;
|
||||||
justify-content: center;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
grid-template-areas: "logo buttons layout";
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: var(--cpd-space-3x);
|
||||||
padding: var(--footerPadding) 0;
|
padding: var(--footerPadding) var(--inline-content-inset);
|
||||||
/* TODO: Un-hardcode these colors */
|
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
360deg,
|
180deg,
|
||||||
#15191e 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
rgba(16, 19, 23, 0.9) 37%,
|
var(--cpd-color-bg-canvas-default) 100%
|
||||||
rgba(16, 19, 23, 0.8) 49.68%,
|
|
||||||
rgba(16, 19, 23, 0.7) 56.68%,
|
|
||||||
rgba(16, 19, 23, 0.427397) 72.92%,
|
|
||||||
rgba(16, 19, 23, 0.257534) 81.06%,
|
|
||||||
rgba(16, 19, 23, 0.136986) 87.29%,
|
|
||||||
rgba(16, 19, 23, 0.0658079) 92.4%,
|
|
||||||
rgba(16, 19, 23, 0) 100%
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.maximised .footer {
|
.logo {
|
||||||
position: absolute;
|
grid-area: logo;
|
||||||
width: 100%;
|
justify-self: start;
|
||||||
bottom: 0;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
padding-inline-start: var(--cpd-space-1x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
grid-area: buttons;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--cpd-space-3x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
grid-area: layout;
|
||||||
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-height: 300px) {
|
@media (min-height: 300px) {
|
||||||
@@ -86,7 +94,7 @@ limitations under the License.
|
|||||||
--footerPadding: 60px;
|
--footerPadding: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.buttons {
|
||||||
gap: 16px;
|
gap: var(--cpd-space-4x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,19 +23,19 @@ import {
|
|||||||
useTracks,
|
useTracks,
|
||||||
} from "@livekit/components-react";
|
} from "@livekit/components-react";
|
||||||
import { usePreventScroll } from "@react-aria/overlays";
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
import classNames from "classnames";
|
import { ConnectionState, Room, Track } from "livekit-client";
|
||||||
import { DisconnectReason, Room, RoomEvent, Track } from "livekit-client";
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
|
||||||
import { Ref, useCallback, useEffect, useMemo, useRef } from "react";
|
import { Ref, useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { RoomEventCallbacks } from "livekit-client/dist/src/room/Room";
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
|
import { ReactComponent as LogoMark } from "../icons/LogoMark.svg";
|
||||||
|
import { ReactComponent as LogoType } from "../icons/LogoType.svg";
|
||||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import {
|
import {
|
||||||
HangupButton,
|
HangupButton,
|
||||||
@@ -43,52 +43,38 @@ import {
|
|||||||
VideoButton,
|
VideoButton,
|
||||||
ScreenshareButton,
|
ScreenshareButton,
|
||||||
SettingsButton,
|
SettingsButton,
|
||||||
InviteButton,
|
|
||||||
} from "../button";
|
} from "../button";
|
||||||
import {
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
Header,
|
|
||||||
LeftNav,
|
|
||||||
RightNav,
|
|
||||||
RoomHeaderInfo,
|
|
||||||
VersionMismatchWarning,
|
|
||||||
} from "../Header";
|
|
||||||
import {
|
import {
|
||||||
useVideoGridLayout,
|
useVideoGridLayout,
|
||||||
TileDescriptor,
|
TileDescriptor,
|
||||||
VideoGrid,
|
VideoGrid,
|
||||||
} from "../video-grid/VideoGrid";
|
} from "../video-grid/VideoGrid";
|
||||||
import {
|
import { useShowConnectionStats } from "../settings/useSetting";
|
||||||
useShowInspector,
|
|
||||||
useShowConnectionStats,
|
|
||||||
} from "../settings/useSetting";
|
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||||
import { useUrlParams } from "../UrlParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||||
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
||||||
import { ElementWidgetActions, widget } from "../widget";
|
import { ElementWidgetActions, widget } from "../widget";
|
||||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
|
||||||
import { GroupCallInspector } from "./GroupCallInspector";
|
|
||||||
import styles from "./InCallView.module.css";
|
import styles from "./InCallView.module.css";
|
||||||
import { useJoinRule } from "./useJoinRule";
|
|
||||||
import { ParticipantInfo } from "./useGroupCall";
|
|
||||||
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
|
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
|
||||||
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
|
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
|
||||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||||
import { SettingsModal } from "../settings/SettingsModal";
|
import { SettingsModal } from "../settings/SettingsModal";
|
||||||
import { InviteModal } from "./InviteModal";
|
|
||||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||||
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
|
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
|
||||||
import { useFullscreen } from "./useFullscreen";
|
import { useFullscreen } from "./useFullscreen";
|
||||||
import { useLayoutStates } from "../video-grid/Layout";
|
import { useLayoutStates } from "../video-grid/Layout";
|
||||||
import { useSFUConfig } from "../livekit/OpenIDLoader";
|
|
||||||
import { E2EELock } from "../E2EELock";
|
|
||||||
import { useEventEmitterThree } from "../useEvents";
|
|
||||||
import { useWakeLock } from "../useWakeLock";
|
import { useWakeLock } from "../useWakeLock";
|
||||||
import { useMergedRefs } from "../useMergedRefs";
|
import { useMergedRefs } from "../useMergedRefs";
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
import { useIsRoomE2EE } from "../e2ee/sharedKeyManagement";
|
import { MatrixInfo } from "./VideoPreview";
|
||||||
|
import { ShareButton } from "../button/ShareButton";
|
||||||
|
import { LayoutToggle } from "./LayoutToggle";
|
||||||
|
import { ECConnectionState } from "../livekit/useECConnectionState";
|
||||||
|
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
@@ -96,13 +82,18 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
|||||||
// For now we can disable screensharing in Safari.
|
// For now we can disable screensharing in Safari.
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> {
|
export interface ActiveCallProps
|
||||||
|
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
|
||||||
e2eeConfig?: E2EEConfig;
|
e2eeConfig?: E2EEConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActiveCall(props: ActiveCallProps) {
|
export function ActiveCall(props: ActiveCallProps) {
|
||||||
const sfuConfig = useSFUConfig();
|
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
||||||
const livekitRoom = useLiveKit(props.muteStates, sfuConfig, props.e2eeConfig);
|
const { livekitRoom, connState } = useLiveKit(
|
||||||
|
props.muteStates,
|
||||||
|
sfuConfig,
|
||||||
|
props.e2eeConfig
|
||||||
|
);
|
||||||
|
|
||||||
if (!livekitRoom) {
|
if (!livekitRoom) {
|
||||||
return null;
|
return null;
|
||||||
@@ -114,39 +105,49 @@ export function ActiveCall(props: ActiveCallProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={livekitRoom}>
|
<RoomContext.Provider value={livekitRoom}>
|
||||||
<InCallView {...props} livekitRoom={livekitRoom} />
|
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InCallViewProps {
|
export interface InCallViewProps {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
groupCall: GroupCall;
|
matrixInfo: MatrixInfo;
|
||||||
|
rtcSession: MatrixRTCSession;
|
||||||
livekitRoom: Room;
|
livekitRoom: Room;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
participatingMembers: RoomMember[];
|
||||||
onLeave: (error?: Error) => void;
|
onLeave: (error?: Error) => void;
|
||||||
unencryptedEventsFromUsers: Set<string>;
|
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||||
|
connState: ECConnectionState;
|
||||||
|
onShareClick: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InCallView({
|
export function InCallView({
|
||||||
client,
|
client,
|
||||||
groupCall,
|
matrixInfo,
|
||||||
|
rtcSession,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
muteStates,
|
muteStates,
|
||||||
participants,
|
participatingMembers,
|
||||||
onLeave,
|
onLeave,
|
||||||
unencryptedEventsFromUsers,
|
|
||||||
hideHeader,
|
hideHeader,
|
||||||
otelGroupCallMembership,
|
otelGroupCallMembership,
|
||||||
|
connState,
|
||||||
|
onShareClick,
|
||||||
}: InCallViewProps) {
|
}: InCallViewProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
|
|
||||||
const isRoomE2EE = useIsRoomE2EE(groupCall.room.roomId);
|
useEffect(() => {
|
||||||
|
if (connState === ConnectionState.Disconnected) {
|
||||||
|
// annoyingly we don't get the disconnection reason this way,
|
||||||
|
// only by listening for the emitted event
|
||||||
|
onLeave(new Error("Disconnected from call server"));
|
||||||
|
}
|
||||||
|
}, [connState, onLeave]);
|
||||||
|
|
||||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||||
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
@@ -164,7 +165,7 @@ export function InCallView({
|
|||||||
screenSharingTracks.length > 0
|
screenSharingTracks.length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showInspector] = useShowInspector();
|
//const [showInspector] = useShowInspector();
|
||||||
const [showConnectionStats] = useShowConnectionStats();
|
const [showConnectionStats] = useShowConnectionStats();
|
||||||
|
|
||||||
const { hideScreensharing } = useUrlParams();
|
const { hideScreensharing } = useUrlParams();
|
||||||
@@ -182,41 +183,22 @@ export function InCallView({
|
|||||||
[muteStates]
|
[muteStates]
|
||||||
);
|
);
|
||||||
|
|
||||||
const joinRule = useJoinRule(groupCall.room);
|
|
||||||
|
|
||||||
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
|
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
|
||||||
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
|
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
|
||||||
useCallViewKeyboardShortcuts(
|
useCallViewKeyboardShortcuts(
|
||||||
containerRef1,
|
containerRef1,
|
||||||
toggleMicrophone,
|
toggleMicrophone,
|
||||||
toggleCamera,
|
toggleCamera,
|
||||||
async (muted) => await localParticipant.setMicrophoneEnabled(!muted)
|
(muted) => muteStates.audio.setEnabled?.(!muted)
|
||||||
);
|
|
||||||
|
|
||||||
const onDisconnected = useCallback(
|
|
||||||
(reason?: DisconnectReason) => {
|
|
||||||
PosthogAnalytics.instance.eventCallDisconnected.track(reason);
|
|
||||||
logger.info("Disconnected from livekit call with reason ", reason);
|
|
||||||
onLeave(
|
|
||||||
new Error("Disconnected from LiveKit call with reason " + reason)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[onLeave]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLeavePress = useCallback(() => {
|
const onLeavePress = useCallback(() => {
|
||||||
onLeave();
|
onLeave();
|
||||||
}, [onLeave]);
|
}, [onLeave]);
|
||||||
|
|
||||||
useEventEmitterThree<RoomEvent.Disconnected, RoomEventCallbacks>(
|
|
||||||
livekitRoom,
|
|
||||||
RoomEvent.Disconnected,
|
|
||||||
onDisconnected
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
widget?.api.transport.send(
|
widget?.api.transport.send(
|
||||||
layout === "freedom"
|
layout === "grid"
|
||||||
? ElementWidgetActions.TileLayout
|
? ElementWidgetActions.TileLayout
|
||||||
: ElementWidgetActions.SpotlightLayout,
|
: ElementWidgetActions.SpotlightLayout,
|
||||||
{}
|
{}
|
||||||
@@ -226,7 +208,7 @@ export function InCallView({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget) {
|
if (widget) {
|
||||||
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
setLayout("freedom");
|
setLayout("grid");
|
||||||
await widget!.api.transport.reply(ev.detail, {});
|
await widget!.api.transport.reply(ev.detail, {});
|
||||||
};
|
};
|
||||||
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
@@ -250,10 +232,11 @@ export function InCallView({
|
|||||||
}
|
}
|
||||||
}, [setLayout]);
|
}, [setLayout]);
|
||||||
|
|
||||||
const reducedControls = boundsValid && bounds.width <= 400;
|
const mobile = boundsValid && bounds.width <= 660;
|
||||||
|
const reducedControls = boundsValid && bounds.width <= 340;
|
||||||
const noControls = reducedControls && bounds.height <= 400;
|
const noControls = reducedControls && bounds.height <= 400;
|
||||||
|
|
||||||
const items = useParticipantTiles(livekitRoom, participants);
|
const items = useParticipantTiles(livekitRoom, rtcSession.room);
|
||||||
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
||||||
useFullscreen(items);
|
useFullscreen(items);
|
||||||
|
|
||||||
@@ -270,7 +253,7 @@ export function InCallView({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const Grid =
|
const Grid =
|
||||||
items.length > 12 && layout === "freedom" ? NewVideoGrid : VideoGrid;
|
items.length > 12 && layout === "grid" ? NewVideoGrid : VideoGrid;
|
||||||
|
|
||||||
const prefersReducedMotion = usePrefersReducedMotion();
|
const prefersReducedMotion = usePrefersReducedMotion();
|
||||||
|
|
||||||
@@ -327,7 +310,7 @@ export function InCallView({
|
|||||||
const {
|
const {
|
||||||
modalState: rageshakeRequestModalState,
|
modalState: rageshakeRequestModalState,
|
||||||
modalProps: rageshakeRequestModalProps,
|
modalProps: rageshakeRequestModalProps,
|
||||||
} = useRageshakeRequestModal(groupCall.room.roomId);
|
} = useRageshakeRequestModal(rtcSession.room.roomId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
modalState: settingsModalState,
|
modalState: settingsModalState,
|
||||||
@@ -344,25 +327,6 @@ export function InCallView({
|
|||||||
settingsModalState.open();
|
settingsModalState.open();
|
||||||
}, [settingsModalState]);
|
}, [settingsModalState]);
|
||||||
|
|
||||||
const {
|
|
||||||
modalState: inviteModalState,
|
|
||||||
modalProps: inviteModalProps,
|
|
||||||
}: {
|
|
||||||
modalState: OverlayTriggerState;
|
|
||||||
modalProps: {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
} = useModalTriggerState();
|
|
||||||
|
|
||||||
const openInvite = useCallback(() => {
|
|
||||||
inviteModalState.open();
|
|
||||||
}, [inviteModalState]);
|
|
||||||
|
|
||||||
const containerClasses = classNames(styles.inRoom, {
|
|
||||||
[styles.maximised]: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleScreensharing = useCallback(async () => {
|
const toggleScreensharing = useCallback(async () => {
|
||||||
exitFullscreen();
|
exitFullscreen();
|
||||||
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
|
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
|
||||||
@@ -420,29 +384,47 @@ export function InCallView({
|
|||||||
buttons.push(
|
buttons.push(
|
||||||
<HangupButton key="6" onPress={onLeavePress} data-testid="incall_leave" />
|
<HangupButton key="6" onPress={onLeavePress} data-testid="incall_leave" />
|
||||||
);
|
);
|
||||||
footer = <div className={styles.footer}>{buttons}</div>;
|
footer = (
|
||||||
|
<div className={styles.footer}>
|
||||||
|
{!mobile && !hideHeader && (
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<LogoMark width={24} height={24} aria-hidden />
|
||||||
|
<LogoType
|
||||||
|
width={80}
|
||||||
|
height={11}
|
||||||
|
aria-label={import.meta.env.VITE_PRODUCT_NAME || "Element Call"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.buttons}>{buttons}</div>
|
||||||
|
{!mobile && !hideHeader && (
|
||||||
|
<LayoutToggle
|
||||||
|
className={styles.layout}
|
||||||
|
layout={layout}
|
||||||
|
setLayout={setLayout}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses} ref={containerRef}>
|
<div className={styles.inRoom} ref={containerRef}>
|
||||||
{!hideHeader && maximisedParticipant === null && (
|
{!hideHeader && maximisedParticipant === null && (
|
||||||
<Header>
|
<Header>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<RoomHeaderInfo roomName={groupCall.room.name} />
|
<RoomHeaderInfo
|
||||||
<VersionMismatchWarning
|
id={matrixInfo.roomId}
|
||||||
users={unencryptedEventsFromUsers}
|
name={matrixInfo.roomName}
|
||||||
room={groupCall.room}
|
avatarUrl={matrixInfo.roomAvatar}
|
||||||
|
encrypted={matrixInfo.roomEncrypted}
|
||||||
|
participants={participatingMembers}
|
||||||
|
client={client}
|
||||||
/>
|
/>
|
||||||
{!isRoomE2EE && <E2EELock />}
|
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
{!reducedControls && onShareClick !== null && (
|
||||||
{joinRule === JoinRule.Public && (
|
<ShareButton data-testid= "call_invite" onClick={onShareClick} />
|
||||||
<InviteButton
|
|
||||||
data-testid="call_invite"
|
|
||||||
variant="icon"
|
|
||||||
onClick={openInvite}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</RightNav>
|
</RightNav>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -452,53 +434,61 @@ export function InCallView({
|
|||||||
{renderContent()}
|
{renderContent()}
|
||||||
{footer}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
{otelGroupCallMembership && (
|
{/*otelGroupCallMembership && (
|
||||||
<GroupCallInspector
|
<GroupCallInspector
|
||||||
client={client}
|
client={client}
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
otelGroupCallMembership={otelGroupCallMembership}
|
otelGroupCallMembership={otelGroupCallMembership}
|
||||||
show={showInspector}
|
show={showInspector}
|
||||||
/>
|
/>
|
||||||
)}
|
)*/}
|
||||||
{rageshakeRequestModalState.isOpen && !noControls && (
|
{rageshakeRequestModalState.isOpen && !noControls && (
|
||||||
<RageshakeRequestModal
|
<RageshakeRequestModal
|
||||||
{...rageshakeRequestModalProps}
|
{...rageshakeRequestModalProps}
|
||||||
roomId={groupCall.room.roomId}
|
roomId={rtcSession.room.roomId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{settingsModalState.isOpen && (
|
{settingsModalState.isOpen && (
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
client={client}
|
client={client}
|
||||||
roomId={groupCall.room.roomId}
|
roomId={rtcSession.room.roomId}
|
||||||
{...settingsModalProps}
|
{...settingsModalProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{inviteModalState.isOpen && (
|
|
||||||
<InviteModal roomId={groupCall.room.roomId} {...inviteModalProps} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findMatrixMember(
|
||||||
|
room: MatrixRoom,
|
||||||
|
id: string
|
||||||
|
): RoomMember | undefined {
|
||||||
|
if (!id) return undefined;
|
||||||
|
|
||||||
|
const parts = id.split(":");
|
||||||
|
// must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon
|
||||||
|
if (parts.length < 3) {
|
||||||
|
logger.warn(
|
||||||
|
"Livekit participants ID doesn't look like a userId:deviceId combination"
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.pop();
|
||||||
|
const userId = parts.join(":");
|
||||||
|
|
||||||
|
return room.getMember(userId) ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function useParticipantTiles(
|
function useParticipantTiles(
|
||||||
livekitRoom: Room,
|
livekitRoom: Room,
|
||||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>
|
matrixRoom: MatrixRoom
|
||||||
): TileDescriptor<ItemData>[] {
|
): TileDescriptor<ItemData>[] {
|
||||||
const sfuParticipants = useParticipants({
|
const sfuParticipants = useParticipants({
|
||||||
room: livekitRoom,
|
room: livekitRoom,
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
// The IDs of the participants who published membership event to the room (i.e. are present from Matrix perspective).
|
|
||||||
const matrixParticipants: Map<string, RoomMember> = new Map(
|
|
||||||
[...participants.entries()].flatMap(([user, devicesMap]) => {
|
|
||||||
return [...devicesMap.keys()].map((deviceId) => [
|
|
||||||
`${user.userId}:${deviceId}`,
|
|
||||||
user,
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasPresenter =
|
const hasPresenter =
|
||||||
sfuParticipants.find((p) => p.isScreenShareEnabled) !== undefined;
|
sfuParticipants.find((p) => p.isScreenShareEnabled) !== undefined;
|
||||||
let allGhosts = true;
|
let allGhosts = true;
|
||||||
@@ -514,7 +504,14 @@ function useParticipantTiles(
|
|||||||
: false;
|
: false;
|
||||||
|
|
||||||
const id = sfuParticipant.identity;
|
const id = sfuParticipant.identity;
|
||||||
const member = matrixParticipants.get(id);
|
const member = findMatrixMember(matrixRoom, id);
|
||||||
|
// We always start with a local participant wit the empty string as their ID before we're
|
||||||
|
// connected, this is fine and we'll be in "all ghosts" mode.
|
||||||
|
if (id !== "" && member === undefined) {
|
||||||
|
logger.warn(
|
||||||
|
`Ruh, roh! No matrix member found for SFU participant '${id}': creating g-g-g-ghost!`
|
||||||
|
);
|
||||||
|
}
|
||||||
allGhosts &&= member === undefined;
|
allGhosts &&= member === undefined;
|
||||||
|
|
||||||
const userMediaTile = {
|
const userMediaTile = {
|
||||||
@@ -566,7 +563,7 @@ function useParticipantTiles(
|
|||||||
// If every item is a ghost, that probably means we're still connecting and
|
// If every item is a ghost, that probably means we're still connecting and
|
||||||
// shouldn't bother showing anything yet
|
// shouldn't bother showing anything yet
|
||||||
return allGhosts ? [] : tiles;
|
return allGhosts ? [] : tiles;
|
||||||
}, [participants, sfuParticipants]);
|
}, [matrixRoom, sfuParticipants]);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/room/LayoutToggle.module.css
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
padding: 2px;
|
||||||
|
border: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||||
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
|
box-shadow: 0px 0px 40px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input {
|
||||||
|
appearance: none;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle label {
|
||||||
|
display: block;
|
||||||
|
padding: calc(2.5 * var(--cpd-space-1x));
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
|
color: var(--cpd-color-icon-primary);
|
||||||
|
background: var(--cpd-color-bg-action-secondary-rest);
|
||||||
|
box-shadow: var(--small-drop-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.toggle label:hover {
|
||||||
|
background: var(--cpd-color-bg-action-secondary-hovered);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle label:active {
|
||||||
|
background: var(--cpd-color-bg-action-secondary-hovered);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:checked + label {
|
||||||
|
color: var(--cpd-color-icon-on-solid-primary);
|
||||||
|
background: var(--cpd-color-bg-action-primary-rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.toggle input:checked + label:hover {
|
||||||
|
background: var(--cpd-color-bg-action-primary-hovered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:checked + label:active {
|
||||||
|
background: var(--cpd-color-bg-action-primary-hovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle label > svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle label:last-child {
|
||||||
|
margin-inline-start: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:focus-visible + label {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
75
src/room/LayoutToggle.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ChangeEvent, FC, useCallback, useId } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
|
import { ReactComponent as SpotlightViewIcon } from "@vector-im/compound-design-tokens/icons/spotlight-view.svg";
|
||||||
|
import { ReactComponent as GridViewIcon } from "@vector-im/compound-design-tokens/icons/grid-view.svg";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import styles from "./LayoutToggle.module.css";
|
||||||
|
|
||||||
|
export type Layout = "spotlight" | "grid";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
layout: Layout;
|
||||||
|
setLayout: (layout: Layout) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout),
|
||||||
|
[setLayout]
|
||||||
|
);
|
||||||
|
|
||||||
|
const spotlightId = useId();
|
||||||
|
const gridId = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.toggle, className)}>
|
||||||
|
<input
|
||||||
|
id={spotlightId}
|
||||||
|
type="radio"
|
||||||
|
name="layout"
|
||||||
|
value="spotlight"
|
||||||
|
checked={layout === "spotlight"}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<Tooltip label={t("Spotlight")}>
|
||||||
|
<label htmlFor={spotlightId}>
|
||||||
|
<SpotlightViewIcon aria-label={t("Spotlight")} />
|
||||||
|
</label>
|
||||||
|
</Tooltip>
|
||||||
|
<input
|
||||||
|
id={gridId}
|
||||||
|
type="radio"
|
||||||
|
name="layout"
|
||||||
|
value="grid"
|
||||||
|
checked={layout === "grid"}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<Tooltip label={t("Grid")}>
|
||||||
|
<label htmlFor={gridId}>
|
||||||
|
<GridViewIcon aria-label={t("Grid")} />
|
||||||
|
</label>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,32 +16,39 @@ limitations under the License.
|
|||||||
|
|
||||||
import { useRef, useEffect, FC } from "react";
|
import { useRef, useEffect, FC } from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import styles from "./LobbyView.module.css";
|
import styles from "./LobbyView.module.css";
|
||||||
import { Button, CopyButton } from "../button";
|
import { Button, CopyButton } from "../button";
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
|
||||||
import { Body, Link } from "../typography/Typography";
|
import { Body, Link } from "../typography/Typography";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
import { ShareButton } from "../button/ShareButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
client: MatrixClient;
|
||||||
matrixInfo: MatrixInfo;
|
matrixInfo: MatrixInfo;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
onEnter: () => void;
|
onEnter: () => void;
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
|
participatingMembers: RoomMember[];
|
||||||
|
onShareClick: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LobbyView: FC<Props> = ({
|
export const LobbyView: FC<Props> = ({
|
||||||
|
client,
|
||||||
matrixInfo,
|
matrixInfo,
|
||||||
muteStates,
|
muteStates,
|
||||||
onEnter,
|
onEnter,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
|
participatingMembers,
|
||||||
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const roomSharedKey = useRoomSharedKey(matrixInfo.roomId);
|
const roomSharedKey = useRoomSharedKey(matrixInfo.roomId);
|
||||||
@@ -59,10 +66,17 @@ export const LobbyView: FC<Props> = ({
|
|||||||
{!hideHeader && (
|
{!hideHeader && (
|
||||||
<Header>
|
<Header>
|
||||||
<LeftNav>
|
<LeftNav>
|
||||||
<RoomHeaderInfo roomName={matrixInfo.roomName} />
|
<RoomHeaderInfo
|
||||||
|
id={matrixInfo.roomId}
|
||||||
|
name={matrixInfo.roomName}
|
||||||
|
avatarUrl={matrixInfo.roomAvatar}
|
||||||
|
encrypted={matrixInfo.roomEncrypted}
|
||||||
|
participants={participatingMembers}
|
||||||
|
client={client}
|
||||||
|
/>
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
<RightNav>
|
<RightNav>
|
||||||
<UserMenuContainer />
|
{onShareClick !== null && <ShareButton onClick={onShareClick} />}
|
||||||
</RightNav>
|
</RightNav>
|
||||||
</Header>
|
</Header>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FC, useEffect, useState, useCallback } from "react";
|
import { FC, useEffect, useState, useCallback } from "react";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import { useClientLegacy } from "../ClientContext";
|
import { useClientLegacy } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
import { RoomAuthView } from "./RoomAuthView";
|
import { RoomAuthView } from "./RoomAuthView";
|
||||||
@@ -73,10 +73,10 @@ export const RoomPage: FC = () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const groupCallView = useCallback(
|
const groupCallView = useCallback(
|
||||||
(groupCall: GroupCall) => (
|
(rtcSession: MatrixRTCSession) => (
|
||||||
<GroupCallView
|
<GroupCallView
|
||||||
client={client!}
|
client={client!}
|
||||||
groupCall={groupCall}
|
rtcSession={rtcSession}
|
||||||
isPasswordlessUser={passwordlessUser}
|
isPasswordlessUser={passwordlessUser}
|
||||||
isEmbedded={isEmbedded}
|
isEmbedded={isEmbedded}
|
||||||
preload={preload}
|
preload={preload}
|
||||||
|
|||||||
@@ -20,20 +20,20 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Modal, ModalContent, ModalProps } from "../Modal";
|
import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import styles from "./InviteModal.module.css";
|
import styles from "./ShareModal.module.css";
|
||||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
|
export const ShareModal: FC<Props> = ({ roomId, ...rest }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const roomSharedKey = useRoomSharedKey(roomId);
|
const roomSharedKey = useRoomSharedKey(roomId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t("Invite people")}
|
title={t("Share this call")}
|
||||||
isDismissable
|
isDismissable
|
||||||
className={styles.inviteModal}
|
className={styles.inviteModal}
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -35,11 +35,14 @@ import { useMediaDevices } from "../livekit/MediaDevicesContext";
|
|||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
|
|
||||||
export type MatrixInfo = {
|
export type MatrixInfo = {
|
||||||
|
userId: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
roomName: string;
|
roomName: string;
|
||||||
roomAlias: string | null;
|
roomAlias: string | null;
|
||||||
|
roomAvatar: string | null;
|
||||||
|
roomEncrypted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -68,6 +71,9 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
|||||||
|
|
||||||
const devices = useMediaDevices();
|
const devices = useMediaDevices();
|
||||||
|
|
||||||
|
// Capture the audio options as they were when we first mounted, because
|
||||||
|
// we're not doing anything with the audio anyway so we don't need to
|
||||||
|
// re-open the devices when they change (see below).
|
||||||
const initialAudioOptions = useRef<CreateLocalTracksOptions["audio"]>();
|
const initialAudioOptions = useRef<CreateLocalTracksOptions["audio"]>();
|
||||||
initialAudioOptions.current ??= muteStates.audio.enabled && {
|
initialAudioOptions.current ??= muteStates.audio.enabled && {
|
||||||
deviceId: devices.audioInput.selectedId,
|
deviceId: devices.audioInput.selectedId,
|
||||||
@@ -79,7 +85,9 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
|||||||
// request over with at the same time. But changing the audio settings
|
// request over with at the same time. But changing the audio settings
|
||||||
// shouldn't cause this hook to recreate the track, which is why we
|
// shouldn't cause this hook to recreate the track, which is why we
|
||||||
// reference the initial values here.
|
// reference the initial values here.
|
||||||
audio: initialAudioOptions.current,
|
// We also pass in a clone because livekit mutates the object passed in,
|
||||||
|
// which would cause the devices to be re-opened on the next render.
|
||||||
|
audio: Object.assign({}, initialAudioOptions.current),
|
||||||
video: muteStates.video.enabled && {
|
video: muteStates.video.enabled && {
|
||||||
deviceId: devices.videoInput.selectedId,
|
deviceId: devices.videoInput.selectedId,
|
||||||
},
|
},
|
||||||
@@ -130,9 +138,10 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
|
|||||||
{!muteStates.video.enabled && (
|
{!muteStates.video.enabled && (
|
||||||
<div className={styles.avatarContainer}>
|
<div className={styles.avatarContainer}>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
id={matrixInfo.userId}
|
||||||
|
name={matrixInfo.displayName}
|
||||||
size={(previewBounds.height - 66) / 2}
|
size={(previewBounds.height - 66) / 2}
|
||||||
src={matrixInfo.avatarUrl}
|
src={matrixInfo.avatarUrl}
|
||||||
fallback={matrixInfo.displayName.slice(0, 1).toUpperCase()}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
68
src/room/useActiveFocus.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
MatrixRTCSession,
|
||||||
|
MatrixRTCSessionEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { deepCompare } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
|
import { LivekitFocus } from "../livekit/LivekitFocus";
|
||||||
|
|
||||||
|
function getActiveFocus(
|
||||||
|
rtcSession: MatrixRTCSession
|
||||||
|
): LivekitFocus | undefined {
|
||||||
|
const oldestMembership = rtcSession.getOldestMembership();
|
||||||
|
return oldestMembership?.getActiveFoci()[0] as LivekitFocus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the currently active (livekit) focus for a MatrixRTC session
|
||||||
|
* This logic is specific to livekit foci where the whole call must use one
|
||||||
|
* and the same focus.
|
||||||
|
*/
|
||||||
|
export function useActiveFocus(
|
||||||
|
rtcSession: MatrixRTCSession
|
||||||
|
): LivekitFocus | undefined {
|
||||||
|
const [activeFocus, setActiveFocus] = useState(() =>
|
||||||
|
getActiveFocus(rtcSession)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMembershipsChanged = useCallback(() => {
|
||||||
|
const newActiveFocus = getActiveFocus(rtcSession);
|
||||||
|
|
||||||
|
if (!deepCompare(activeFocus, newActiveFocus)) {
|
||||||
|
setActiveFocus(newActiveFocus);
|
||||||
|
}
|
||||||
|
}, [activeFocus, rtcSession]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rtcSession.on(
|
||||||
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
|
onMembershipsChanged
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rtcSession.off(
|
||||||
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
|
onMembershipsChanged
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return activeFocus;
|
||||||
|
}
|
||||||
@@ -1,644 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useReducer, useState } from "react";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import {
|
|
||||||
GroupCallEvent,
|
|
||||||
GroupCallState,
|
|
||||||
GroupCall,
|
|
||||||
GroupCallErrorCode,
|
|
||||||
GroupCallUnknownDeviceError,
|
|
||||||
GroupCallError,
|
|
||||||
GroupCallStatsReportEvent,
|
|
||||||
GroupCallStatsReport,
|
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
|
||||||
import { MatrixClient, RoomStateEvent } from "matrix-js-sdk";
|
|
||||||
import {
|
|
||||||
ByteSentStatsReport,
|
|
||||||
ConnectionStatsReport,
|
|
||||||
SummaryStatsReport,
|
|
||||||
CallFeedReport,
|
|
||||||
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
|
||||||
|
|
||||||
import { usePageUnload } from "./usePageUnload";
|
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
|
||||||
import { TranslatedError, translatedError } from "../TranslatedError";
|
|
||||||
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
|
||||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
|
||||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
|
||||||
import { checkForParallelCalls } from "./checkForParallelCalls";
|
|
||||||
|
|
||||||
enum ConnectionState {
|
|
||||||
EstablishingCall = "establishing call", // call hasn't been established yet
|
|
||||||
WaitMedia = "wait_media", // call is set up, waiting for ICE to connect
|
|
||||||
Connected = "connected", // media is flowing
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParticipantInfo {
|
|
||||||
connectionState: ConnectionState;
|
|
||||||
presenter: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseGroupCallReturnType {
|
|
||||||
state: GroupCallState;
|
|
||||||
localCallFeed?: CallFeed;
|
|
||||||
activeSpeaker?: CallFeed;
|
|
||||||
userMediaFeeds: CallFeed[];
|
|
||||||
microphoneMuted: boolean;
|
|
||||||
localVideoMuted: boolean;
|
|
||||||
error?: TranslatedError;
|
|
||||||
initLocalCallFeed: () => void;
|
|
||||||
enter: () => Promise<void>;
|
|
||||||
leave: () => void;
|
|
||||||
toggleLocalVideoMuted: () => void;
|
|
||||||
toggleMicrophoneMuted: () => void;
|
|
||||||
toggleScreensharing: () => void;
|
|
||||||
setMicrophoneMuted: (muted: boolean) => void;
|
|
||||||
requestingScreenshare: boolean;
|
|
||||||
isScreensharing: boolean;
|
|
||||||
screenshareFeeds: CallFeed[];
|
|
||||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
|
||||||
hasLocalParticipant: boolean;
|
|
||||||
unencryptedEventsFromUsers: Set<string>;
|
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
state: GroupCallState;
|
|
||||||
localCallFeed?: CallFeed;
|
|
||||||
activeSpeaker?: CallFeed;
|
|
||||||
userMediaFeeds: CallFeed[];
|
|
||||||
error?: TranslatedError;
|
|
||||||
microphoneMuted: boolean;
|
|
||||||
localVideoMuted: boolean;
|
|
||||||
screenshareFeeds: CallFeed[];
|
|
||||||
isScreensharing: boolean;
|
|
||||||
requestingScreenshare: boolean;
|
|
||||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
|
||||||
hasLocalParticipant: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a bit of a hack, but we keep the opentelemetry tracker object at the file
|
|
||||||
// level so that it doesn't pop in & out of existence as react mounts & unmounts
|
|
||||||
// components. The right solution is probably for this to live in the js-sdk and have
|
|
||||||
// the same lifetime as groupcalls themselves.
|
|
||||||
let groupCallOTelMembership: OTelGroupCallMembership | undefined;
|
|
||||||
let groupCallOTelMembershipGroupCallId: string;
|
|
||||||
|
|
||||||
function getParticipants(
|
|
||||||
groupCall: GroupCall
|
|
||||||
): Map<RoomMember, Map<string, ParticipantInfo>> {
|
|
||||||
const participants = new Map<RoomMember, Map<string, ParticipantInfo>>();
|
|
||||||
|
|
||||||
for (const [member, participantsStateMap] of groupCall.participants) {
|
|
||||||
const participantInfoMap = new Map<string, ParticipantInfo>();
|
|
||||||
participants.set(member, participantInfoMap);
|
|
||||||
|
|
||||||
for (const [deviceId, participant] of participantsStateMap) {
|
|
||||||
const feed = groupCall.userMediaFeeds.find(
|
|
||||||
(f) => f.userId === member.userId && f.deviceId === deviceId
|
|
||||||
);
|
|
||||||
|
|
||||||
let connectionState: ConnectionState;
|
|
||||||
// If we allow calls without media, we have no feeds and cannot read the connection status from them.
|
|
||||||
// @TODO: The connection state should generally not be determined by the feed.
|
|
||||||
if (
|
|
||||||
groupCall.allowCallWithoutVideoAndAudio &&
|
|
||||||
!feed &&
|
|
||||||
!participant.screensharing
|
|
||||||
) {
|
|
||||||
connectionState = ConnectionState.Connected;
|
|
||||||
} else {
|
|
||||||
connectionState = feed
|
|
||||||
? feed.connected
|
|
||||||
? ConnectionState.Connected
|
|
||||||
: ConnectionState.WaitMedia
|
|
||||||
: ConnectionState.EstablishingCall;
|
|
||||||
}
|
|
||||||
participantInfoMap.set(deviceId, {
|
|
||||||
connectionState,
|
|
||||||
presenter: participant.screensharing,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return participants;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGroupCall(
|
|
||||||
groupCall: GroupCall,
|
|
||||||
client: MatrixClient
|
|
||||||
): UseGroupCallReturnType {
|
|
||||||
const [
|
|
||||||
{
|
|
||||||
state,
|
|
||||||
localCallFeed,
|
|
||||||
activeSpeaker,
|
|
||||||
userMediaFeeds,
|
|
||||||
error,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
isScreensharing,
|
|
||||||
screenshareFeeds,
|
|
||||||
participants,
|
|
||||||
hasLocalParticipant,
|
|
||||||
requestingScreenshare,
|
|
||||||
},
|
|
||||||
setState,
|
|
||||||
] = useState<State>({
|
|
||||||
state: GroupCallState.LocalCallFeedUninitialized,
|
|
||||||
userMediaFeeds: [],
|
|
||||||
microphoneMuted: false,
|
|
||||||
localVideoMuted: false,
|
|
||||||
isScreensharing: false,
|
|
||||||
screenshareFeeds: [],
|
|
||||||
requestingScreenshare: false,
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
hasLocalParticipant: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) {
|
|
||||||
if (groupCallOTelMembership) groupCallOTelMembership.dispose();
|
|
||||||
|
|
||||||
// If the user disables analytics, this will stay around until they leave the call
|
|
||||||
// so analytics will be disabled once they leave.
|
|
||||||
if (ElementCallOpenTelemetry.instance) {
|
|
||||||
groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client);
|
|
||||||
groupCallOTelMembershipGroupCallId = groupCall.groupCallId;
|
|
||||||
} else {
|
|
||||||
groupCallOTelMembership = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer(
|
|
||||||
(state: Set<string>, newVal: string) => {
|
|
||||||
return new Set(state).add(newVal);
|
|
||||||
},
|
|
||||||
new Set<string>()
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateState = useCallback(
|
|
||||||
(state: Partial<State>) => setState((prev) => ({ ...prev, ...state })),
|
|
||||||
[setState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doNothingMediaActionCallback = useCallback(
|
|
||||||
(details: MediaSessionActionDetails) => {},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const leaveCall = useCallback(() => {
|
|
||||||
groupCallOTelMembership?.onLeaveCall();
|
|
||||||
groupCall.leave();
|
|
||||||
}, [groupCall]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// disable the media action keys, otherwise audio elements get paused when
|
|
||||||
// the user presses media keys or unplugs headphones, etc.
|
|
||||||
// Note there are actions for muting / unmuting a microphone & hanging up
|
|
||||||
// which we could wire up.
|
|
||||||
const mediaActions: MediaSessionAction[] = [
|
|
||||||
"play",
|
|
||||||
"pause",
|
|
||||||
"stop",
|
|
||||||
"nexttrack",
|
|
||||||
"previoustrack",
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const mediaAction of mediaActions) {
|
|
||||||
navigator.mediaSession?.setActionHandler(
|
|
||||||
mediaAction,
|
|
||||||
doNothingMediaActionCallback
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const mediaAction of mediaActions) {
|
|
||||||
navigator.mediaSession?.setActionHandler(mediaAction, null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [doNothingMediaActionCallback]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function onGroupCallStateChanged() {
|
|
||||||
updateState({
|
|
||||||
state: groupCall.state,
|
|
||||||
localCallFeed: groupCall.localCallFeed,
|
|
||||||
activeSpeaker: groupCall.activeSpeaker,
|
|
||||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
|
||||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
|
||||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
|
||||||
isScreensharing: groupCall.isScreensharing(),
|
|
||||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevUserMediaFeeds = new Set<CallFeed>();
|
|
||||||
|
|
||||||
function onUserMediaFeedsChanged(userMediaFeeds: CallFeed[]): void {
|
|
||||||
for (const feed of prevUserMediaFeeds) {
|
|
||||||
feed.off(CallFeedEvent.ConnectedChanged, onConnectedChanged);
|
|
||||||
}
|
|
||||||
prevUserMediaFeeds.clear();
|
|
||||||
|
|
||||||
for (const feed of userMediaFeeds) {
|
|
||||||
feed.on(CallFeedEvent.ConnectedChanged, onConnectedChanged);
|
|
||||||
prevUserMediaFeeds.add(feed);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({
|
|
||||||
userMediaFeeds: [...userMediaFeeds],
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevScreenshareFeeds = new Set<CallFeed>();
|
|
||||||
|
|
||||||
function onScreenshareFeedsChanged(screenshareFeeds: CallFeed[]): void {
|
|
||||||
for (const feed of prevScreenshareFeeds) {
|
|
||||||
feed.off(CallFeedEvent.ConnectedChanged, onConnectedChanged);
|
|
||||||
}
|
|
||||||
prevScreenshareFeeds.clear();
|
|
||||||
|
|
||||||
for (const feed of screenshareFeeds) {
|
|
||||||
feed.on(CallFeedEvent.ConnectedChanged, onConnectedChanged);
|
|
||||||
prevScreenshareFeeds.add(feed);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState({
|
|
||||||
screenshareFeeds: [...screenshareFeeds],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onConnectedChanged(connected: boolean): void {
|
|
||||||
updateState({
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void {
|
|
||||||
updateState({
|
|
||||||
activeSpeaker: activeSpeaker,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLocalMuteStateChanged(
|
|
||||||
microphoneMuted: boolean,
|
|
||||||
localVideoMuted: boolean
|
|
||||||
): void {
|
|
||||||
updateState({
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLocalScreenshareStateChanged(
|
|
||||||
isScreensharing: boolean,
|
|
||||||
_localScreenshareFeed?: CallFeed,
|
|
||||||
localDesktopCapturerSourceId?: string
|
|
||||||
): void {
|
|
||||||
updateState({
|
|
||||||
isScreensharing,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCallsChanged(): void {
|
|
||||||
updateState({ participants: getParticipants(groupCall) });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onParticipantsChanged(): void {
|
|
||||||
updateState({
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(e: GroupCallError): void {
|
|
||||||
Sentry.captureException(e);
|
|
||||||
if (e.code === GroupCallErrorCode.UnknownDevice) {
|
|
||||||
const unknownDeviceError = e as GroupCallUnknownDeviceError;
|
|
||||||
addUnencryptedEventUser(unknownDeviceError.userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onConnectionStatsReport(
|
|
||||||
report: GroupCallStatsReport<ConnectionStatsReport>
|
|
||||||
): void {
|
|
||||||
groupCallOTelMembership?.onConnectionStatsReport(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onByteSentStatsReport(
|
|
||||||
report: GroupCallStatsReport<ByteSentStatsReport>
|
|
||||||
): void {
|
|
||||||
groupCallOTelMembership?.onByteSentStatsReport(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSummaryStatsReport(
|
|
||||||
report: GroupCallStatsReport<SummaryStatsReport>
|
|
||||||
): void {
|
|
||||||
groupCallOTelMembership?.onSummaryStatsReport(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCallFeedStatsReport(
|
|
||||||
report: GroupCallStatsReport<CallFeedReport>
|
|
||||||
): void {
|
|
||||||
groupCallOTelMembership?.onCallFeedStatsReport(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
|
|
||||||
groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallEvent.ScreenshareFeedsChanged,
|
|
||||||
onScreenshareFeedsChanged
|
|
||||||
);
|
|
||||||
groupCall.on(GroupCallEvent.ActiveSpeakerChanged, onActiveSpeakerChanged);
|
|
||||||
groupCall.on(GroupCallEvent.LocalMuteStateChanged, onLocalMuteStateChanged);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallEvent.LocalScreenshareStateChanged,
|
|
||||||
onLocalScreenshareStateChanged
|
|
||||||
);
|
|
||||||
groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged);
|
|
||||||
groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged);
|
|
||||||
groupCall.on(GroupCallEvent.Error, onError);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallStatsReportEvent.ConnectionStats,
|
|
||||||
onConnectionStatsReport
|
|
||||||
);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallStatsReportEvent.ByteSentStats,
|
|
||||||
onByteSentStatsReport
|
|
||||||
);
|
|
||||||
groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport);
|
|
||||||
groupCall.on(
|
|
||||||
GroupCallStatsReportEvent.CallFeedStats,
|
|
||||||
onCallFeedStatsReport
|
|
||||||
);
|
|
||||||
|
|
||||||
groupCall.room.currentState.on(
|
|
||||||
RoomStateEvent.Update,
|
|
||||||
checkForParallelCalls
|
|
||||||
);
|
|
||||||
|
|
||||||
updateState({
|
|
||||||
error: undefined,
|
|
||||||
state: groupCall.state,
|
|
||||||
localCallFeed: groupCall.localCallFeed,
|
|
||||||
activeSpeaker: groupCall.activeSpeaker,
|
|
||||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
|
||||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
|
||||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
|
||||||
isScreensharing: groupCall.isScreensharing(),
|
|
||||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
|
||||||
participants: getParticipants(groupCall),
|
|
||||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.GroupCallStateChanged,
|
|
||||||
onGroupCallStateChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.UserMediaFeedsChanged,
|
|
||||||
onUserMediaFeedsChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.ScreenshareFeedsChanged,
|
|
||||||
onScreenshareFeedsChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.ActiveSpeakerChanged,
|
|
||||||
onActiveSpeakerChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.LocalMuteStateChanged,
|
|
||||||
onLocalMuteStateChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.LocalScreenshareStateChanged,
|
|
||||||
onLocalScreenshareStateChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(GroupCallEvent.CallsChanged, onCallsChanged);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallEvent.ParticipantsChanged,
|
|
||||||
onParticipantsChanged
|
|
||||||
);
|
|
||||||
groupCall.removeListener(GroupCallEvent.Error, onError);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallStatsReportEvent.ConnectionStats,
|
|
||||||
onConnectionStatsReport
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallStatsReportEvent.ByteSentStats,
|
|
||||||
onByteSentStatsReport
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallStatsReportEvent.SummaryStats,
|
|
||||||
onSummaryStatsReport
|
|
||||||
);
|
|
||||||
groupCall.removeListener(
|
|
||||||
GroupCallStatsReportEvent.CallFeedStats,
|
|
||||||
onCallFeedStatsReport
|
|
||||||
);
|
|
||||||
groupCall.room.currentState.off(
|
|
||||||
RoomStateEvent.Update,
|
|
||||||
checkForParallelCalls
|
|
||||||
);
|
|
||||||
leaveCall();
|
|
||||||
};
|
|
||||||
}, [groupCall, updateState, leaveCall]);
|
|
||||||
|
|
||||||
usePageUnload(() => {
|
|
||||||
leaveCall();
|
|
||||||
});
|
|
||||||
|
|
||||||
const initLocalCallFeed = useCallback(
|
|
||||||
() => groupCall.initLocalCallFeed(),
|
|
||||||
[groupCall]
|
|
||||||
);
|
|
||||||
|
|
||||||
const enter = useCallback(async () => {
|
|
||||||
if (
|
|
||||||
groupCall.state !== GroupCallState.LocalCallFeedUninitialized &&
|
|
||||||
groupCall.state !== GroupCallState.LocalCallFeedInitialized
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
|
||||||
|
|
||||||
// This must be called before we start trying to join the call, as we need to
|
|
||||||
// have started tracking by the time calls start getting created.
|
|
||||||
groupCallOTelMembership?.onJoinCall();
|
|
||||||
|
|
||||||
await groupCall.enter().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
updateState({ error });
|
|
||||||
});
|
|
||||||
}, [groupCall, updateState]);
|
|
||||||
|
|
||||||
const toggleLocalVideoMuted = useCallback(() => {
|
|
||||||
const toggleToMute = !groupCall.isLocalVideoMuted();
|
|
||||||
groupCall.setLocalVideoMuted(toggleToMute);
|
|
||||||
groupCallOTelMembership?.onToggleLocalVideoMuted(toggleToMute);
|
|
||||||
// TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter?
|
|
||||||
PosthogAnalytics.instance.eventMuteCamera.track(
|
|
||||||
toggleToMute,
|
|
||||||
groupCall.groupCallId
|
|
||||||
);
|
|
||||||
}, [groupCall]);
|
|
||||||
|
|
||||||
const setMicrophoneMuted = useCallback(
|
|
||||||
(setMuted: boolean) => {
|
|
||||||
groupCall.setMicrophoneMuted(setMuted);
|
|
||||||
groupCallOTelMembership?.onSetMicrophoneMuted(setMuted);
|
|
||||||
PosthogAnalytics.instance.eventMuteMicrophone.track(
|
|
||||||
setMuted,
|
|
||||||
groupCall.groupCallId
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[groupCall]
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleMicrophoneMuted = useCallback(() => {
|
|
||||||
const toggleToMute = !groupCall.isMicrophoneMuted();
|
|
||||||
groupCallOTelMembership?.onToggleMicrophoneMuted(toggleToMute);
|
|
||||||
setMicrophoneMuted(toggleToMute);
|
|
||||||
}, [groupCall, setMicrophoneMuted]);
|
|
||||||
|
|
||||||
const toggleScreensharing = useCallback(async () => {
|
|
||||||
groupCallOTelMembership?.onToggleScreensharing(!groupCall.isScreensharing);
|
|
||||||
|
|
||||||
if (!groupCall.isScreensharing()) {
|
|
||||||
// toggling on
|
|
||||||
updateState({ requestingScreenshare: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await groupCall.setScreensharingEnabled(true, {
|
|
||||||
audio: true,
|
|
||||||
throwOnFail: true,
|
|
||||||
});
|
|
||||||
updateState({ requestingScreenshare: false });
|
|
||||||
} catch (e) {
|
|
||||||
// this will fail in Electron because getDisplayMedia just throws a permission
|
|
||||||
// error, so if we have a widget API, try requesting via that.
|
|
||||||
if (widget) {
|
|
||||||
const reply = await widget.api.transport.send(
|
|
||||||
ElementWidgetActions.ScreenshareRequest,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
if (!reply.pending) {
|
|
||||||
updateState({ requestingScreenshare: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// toggling off
|
|
||||||
groupCall.setScreensharingEnabled(false);
|
|
||||||
}
|
|
||||||
}, [groupCall, updateState]);
|
|
||||||
|
|
||||||
const onScreenshareStart = useCallback(
|
|
||||||
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
|
||||||
updateState({ requestingScreenshare: false });
|
|
||||||
|
|
||||||
const data = ev.detail.data as unknown as ScreenshareStartData;
|
|
||||||
|
|
||||||
await groupCall.setScreensharingEnabled(true, {
|
|
||||||
desktopCapturerSourceId: data.desktopCapturerSourceId as string,
|
|
||||||
audio: !data.desktopCapturerSourceId,
|
|
||||||
});
|
|
||||||
await widget?.api.transport.reply(ev.detail, {});
|
|
||||||
},
|
|
||||||
[groupCall, updateState]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onScreenshareStop = useCallback(
|
|
||||||
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
|
||||||
updateState({ requestingScreenshare: false });
|
|
||||||
await groupCall.setScreensharingEnabled(false);
|
|
||||||
await widget?.api.transport.reply(ev.detail, {});
|
|
||||||
},
|
|
||||||
[groupCall, updateState]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (widget) {
|
|
||||||
widget.lazyActions.on(
|
|
||||||
ElementWidgetActions.ScreenshareStart,
|
|
||||||
onScreenshareStart
|
|
||||||
);
|
|
||||||
widget.lazyActions.on(
|
|
||||||
ElementWidgetActions.ScreenshareStop,
|
|
||||||
onScreenshareStop
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
widget?.lazyActions.off(
|
|
||||||
ElementWidgetActions.ScreenshareStart,
|
|
||||||
onScreenshareStart
|
|
||||||
);
|
|
||||||
widget?.lazyActions.off(
|
|
||||||
ElementWidgetActions.ScreenshareStop,
|
|
||||||
onScreenshareStop
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [onScreenshareStart, onScreenshareStop]);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (window.RTCPeerConnection === undefined) {
|
|
||||||
const error = translatedError(
|
|
||||||
"WebRTC is not supported or is being blocked in this browser.",
|
|
||||||
t
|
|
||||||
);
|
|
||||||
console.error(error);
|
|
||||||
updateState({ error });
|
|
||||||
}
|
|
||||||
}, [t, updateState]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
localCallFeed,
|
|
||||||
activeSpeaker,
|
|
||||||
userMediaFeeds,
|
|
||||||
microphoneMuted,
|
|
||||||
localVideoMuted,
|
|
||||||
error,
|
|
||||||
initLocalCallFeed,
|
|
||||||
enter,
|
|
||||||
leave: leaveCall,
|
|
||||||
toggleLocalVideoMuted,
|
|
||||||
toggleMicrophoneMuted,
|
|
||||||
toggleScreensharing,
|
|
||||||
setMicrophoneMuted,
|
|
||||||
requestingScreenshare,
|
|
||||||
isScreensharing,
|
|
||||||
screenshareFeeds,
|
|
||||||
participants,
|
|
||||||
hasLocalParticipant,
|
|
||||||
unencryptedEventsFromUsers,
|
|
||||||
otelGroupCallMembership: groupCallOTelMembership,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -15,32 +15,23 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
|
||||||
import {
|
|
||||||
GroupCallType,
|
|
||||||
GroupCallIntent,
|
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { setLocalStorageItem } from "../useLocalStorage";
|
import { setLocalStorageItem } from "../useLocalStorage";
|
||||||
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||||
import { translatedError } from "../TranslatedError";
|
|
||||||
import { widget } from "../widget";
|
|
||||||
import { useEnableE2EE } from "../settings/useSetting";
|
import { useEnableE2EE } from "../settings/useSetting";
|
||||||
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
|
|
||||||
|
|
||||||
export type GroupCallLoaded = {
|
export type GroupCallLoaded = {
|
||||||
kind: "loaded";
|
kind: "loaded";
|
||||||
groupCall: GroupCall;
|
rtcSession: MatrixRTCSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GroupCallLoadFailed = {
|
export type GroupCallLoadFailed = {
|
||||||
@@ -130,61 +121,12 @@ export const useLoadGroupCall = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchOrCreateGroupCall = async (): Promise<GroupCall> => {
|
const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
|
||||||
const room = await fetchOrCreateRoom();
|
const room = await fetchOrCreateRoom();
|
||||||
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
||||||
let groupCall = client.getGroupCallForRoom(room.roomId);
|
|
||||||
logger.debug("Got group call", groupCall?.groupCallId);
|
|
||||||
|
|
||||||
if (groupCall) {
|
const rtcSession = client.matrixRTC.getRoomSession(room);
|
||||||
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
|
return rtcSession;
|
||||||
return groupCall;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!widget &&
|
|
||||||
room.currentState.mayClientSendStateEvent(
|
|
||||||
EventType.GroupCallPrefix,
|
|
||||||
client
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// The call doesn't exist, but we can create it
|
|
||||||
console.log(
|
|
||||||
`No call found in ${roomIdOrAlias}: creating ${
|
|
||||||
createPtt ? "PTT" : "video"
|
|
||||||
} call`
|
|
||||||
);
|
|
||||||
groupCall = await client.createGroupCall(
|
|
||||||
room.roomId,
|
|
||||||
createPtt ? GroupCallType.Voice : GroupCallType.Video,
|
|
||||||
createPtt,
|
|
||||||
GroupCallIntent.Room
|
|
||||||
);
|
|
||||||
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
|
|
||||||
return groupCall;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't have permission to create the call, so all we can do is wait
|
|
||||||
// for one to come in
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const onGroupCallIncoming = (groupCall: GroupCall) => {
|
|
||||||
if (groupCall?.room.roomId === room.roomId) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
groupCall.setGroupCallStatsInterval(STATS_COLLECT_INTERVAL_TIME_MS);
|
|
||||||
client.off(
|
|
||||||
GroupCallEventHandlerEvent.Incoming,
|
|
||||||
onGroupCallIncoming
|
|
||||||
);
|
|
||||||
resolve(groupCall);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
client.on(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming);
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
client.off(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming);
|
|
||||||
reject(translatedError("Fetching group call timed out.", t));
|
|
||||||
}, 30000);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForClientSyncing = async () => {
|
const waitForClientSyncing = async () => {
|
||||||
@@ -207,7 +149,7 @@ export const useLoadGroupCall = (
|
|||||||
|
|
||||||
waitForClientSyncing()
|
waitForClientSyncing()
|
||||||
.then(fetchOrCreateGroupCall)
|
.then(fetchOrCreateGroupCall)
|
||||||
.then((groupCall) => setState({ kind: "loaded", groupCall }))
|
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
||||||
.catch((error) => setState({ kind: "failed", error }));
|
.catch((error) => setState({ kind: "failed", error }));
|
||||||
}, [client, roomIdOrAlias, viaServers, createPtt, t, e2eeEnabled]);
|
}, [client, roomIdOrAlias, viaServers, createPtt, t, e2eeEnabled]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2022 New Vector Ltd
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@@ -14,6 +14,14 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.hideFocus * {
|
import { Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||||
outline: none !important;
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useTypedEventEmitter } from "../useEvents";
|
||||||
|
|
||||||
|
export function useRoomName(room: Room): string {
|
||||||
|
const [, setNumUpdates] = useState(0);
|
||||||
|
// Whenever the name changes, force an update
|
||||||
|
useTypedEventEmitter(room, RoomEvent.Name, () => setNumUpdates((n) => n + 1));
|
||||||
|
return room.name;
|
||||||
}
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { GroupCall, GroupCallEvent } from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
import { CallEvent, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
|
||||||
|
|
||||||
export function useSentryGroupCallHandler(groupCall: GroupCall) {
|
|
||||||
useEffect(() => {
|
|
||||||
function onHangup(call: MatrixCall) {
|
|
||||||
if (call.hangupReason === "ice_failed") {
|
|
||||||
Sentry.captureException(new Error("Call hangup due to ICE failure."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(error: Error) {
|
|
||||||
Sentry.captureException(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupCall) {
|
|
||||||
groupCall.on(CallEvent.Hangup, onHangup);
|
|
||||||
groupCall.on(GroupCallEvent.Error, onError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (groupCall) {
|
|
||||||
groupCall.removeListener(CallEvent.Hangup, onHangup);
|
|
||||||
groupCall.removeListener(GroupCallEvent.Error, onError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [groupCall]);
|
|
||||||
}
|
|
||||||
53
src/rtcSessionHelpers.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
|
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
||||||
|
import { LivekitFocus } from "./livekit/LivekitFocus";
|
||||||
|
import { Config } from "./config/Config";
|
||||||
|
|
||||||
|
function makeFocus(livekitAlias: string): LivekitFocus {
|
||||||
|
const urlFromConf = Config.get().livekit!.livekit_service_url;
|
||||||
|
if (!urlFromConf) {
|
||||||
|
throw new Error("No livekit_service_url is configured!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "livekit",
|
||||||
|
livekit_service_url: urlFromConf,
|
||||||
|
livekit_alias: livekitAlias,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enterRTCSession(rtcSession: MatrixRTCSession) {
|
||||||
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||||
|
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
||||||
|
|
||||||
|
// This must be called before we start trying to join the call, as we need to
|
||||||
|
// have started tracking by the time calls start getting created.
|
||||||
|
//groupCallOTelMembership?.onJoinCall();
|
||||||
|
|
||||||
|
// right now we asume everything is a room-scoped call
|
||||||
|
const livekitAlias = rtcSession.room.roomId;
|
||||||
|
|
||||||
|
rtcSession.joinRoomSession([makeFocus(livekitAlias)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function leaveRTCSession(rtcSession: MatrixRTCSession) {
|
||||||
|
//groupCallOTelMembership?.onLeaveCall();
|
||||||
|
rtcSession.leaveRoomSession();
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ interface Props {
|
|||||||
export function ProfileSettingsTab({ client }: Props) {
|
export function ProfileSettingsTab({ client }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { error, displayName, avatarUrl, saveProfile } = useProfile(client);
|
const { error, displayName, avatarUrl, saveProfile } = useProfile(client);
|
||||||
|
const userId = useMemo(() => client.getUserId(), [client]);
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement | null>(null);
|
const formRef = useRef<HTMLFormElement | null>(null);
|
||||||
|
|
||||||
@@ -77,12 +78,13 @@ export function ProfileSettingsTab({ client }: Props) {
|
|||||||
return (
|
return (
|
||||||
<form onChange={onFormChange} ref={formRef} className={styles.content}>
|
<form onChange={onFormChange} ref={formRef} className={styles.content}>
|
||||||
<FieldRow className={styles.avatarFieldRow}>
|
<FieldRow className={styles.avatarFieldRow}>
|
||||||
{displayName && (
|
{userId && displayName && (
|
||||||
<AvatarInputField
|
<AvatarInputField
|
||||||
id="avatar"
|
id="avatar"
|
||||||
name="avatar"
|
name="avatar"
|
||||||
label={t("Avatar")}
|
label={t("Avatar")}
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
|
userId={userId}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
onRemoveAvatar={onRemoveAvatar}
|
onRemoveAvatar={onRemoveAvatar}
|
||||||
/>
|
/>
|
||||||
|
|||||||
50
src/useMatrixRTCSessionJoinState.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import {
|
||||||
|
MatrixRTCSession,
|
||||||
|
MatrixRTCSessionEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useMatrixRTCSessionJoinState(
|
||||||
|
rtcSession: MatrixRTCSession
|
||||||
|
): boolean {
|
||||||
|
const [isJoined, setJoined] = useState(rtcSession.isJoined());
|
||||||
|
|
||||||
|
const onJoinStateChanged = useCallback(() => {
|
||||||
|
logger.info(
|
||||||
|
`Session in room ${rtcSession.room.roomId} changed to ${
|
||||||
|
rtcSession.isJoined() ? "joined" : "left"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
setJoined(rtcSession.isJoined());
|
||||||
|
}, [rtcSession]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rtcSession.off(
|
||||||
|
MatrixRTCSessionEvent.JoinStateChanged,
|
||||||
|
onJoinStateChanged
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [rtcSession, onJoinStateChanged]);
|
||||||
|
|
||||||
|
return isJoined;
|
||||||
|
}
|
||||||
52
src/useMatrixRTCSessionMemberships.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership";
|
||||||
|
import {
|
||||||
|
MatrixRTCSession,
|
||||||
|
MatrixRTCSessionEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useMatrixRTCSessionMemberships(
|
||||||
|
rtcSession: MatrixRTCSession
|
||||||
|
): CallMembership[] {
|
||||||
|
const [memberships, setMemberships] = useState(rtcSession.memberships);
|
||||||
|
|
||||||
|
const onMembershipsChanged = useCallback(() => {
|
||||||
|
logger.info(
|
||||||
|
`Memberships changed for call in room ${rtcSession.room.roomId} (${rtcSession.memberships.length} members)`
|
||||||
|
);
|
||||||
|
setMemberships(rtcSession.memberships);
|
||||||
|
}, [rtcSession]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rtcSession.on(
|
||||||
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
|
onMembershipsChanged
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rtcSession.off(
|
||||||
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
|
onMembershipsChanged
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [rtcSession, onMembershipsChanged]);
|
||||||
|
|
||||||
|
return memberships;
|
||||||
|
}
|
||||||
37
src/useMediaQuery.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { useEventTarget } from "./useEvents";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook that tracks whether the given media query matches.
|
||||||
|
*/
|
||||||
|
export const useMediaQuery = (query: string) => {
|
||||||
|
const mediaQuery = useMemo(() => matchMedia(query), [query]);
|
||||||
|
|
||||||
|
const [numChanges, setNumChanges] = useState(0);
|
||||||
|
useEventTarget(
|
||||||
|
mediaQuery,
|
||||||
|
"change",
|
||||||
|
useCallback(() => setNumChanges((n) => n + 1), [setNumChanges])
|
||||||
|
);
|
||||||
|
|
||||||
|
// We want any change to the update counter to trigger an update here
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
return useMemo(() => mediaQuery.matches, [mediaQuery, numChanges]);
|
||||||
|
};
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2022 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useFocusVisible } from "@react-aria/interactions";
|
|
||||||
|
|
||||||
import styles from "./usePageFocusStyle.module.css";
|
|
||||||
|
|
||||||
export function usePageFocusStyle(): void {
|
|
||||||
const { isFocusVisible } = useFocusVisible();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const classList = document.body.classList;
|
|
||||||
const hasClass = classList.contains(styles.hideFocus);
|
|
||||||
|
|
||||||
if (isFocusVisible && hasClass) {
|
|
||||||
classList.remove(styles.hideFocus);
|
|
||||||
} else if (!isFocusVisible && !hasClass) {
|
|
||||||
classList.add(styles.hideFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
classList.remove(styles.hideFocus);
|
|
||||||
};
|
|
||||||
}, [isFocusVisible]);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2022 New Vector Ltd
|
Copyright 2022 - 2023 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,29 +14,10 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useMediaQuery } from "./useMediaQuery";
|
||||||
|
|
||||||
import { useEventTarget } from "./useEvents";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns Whether the user has requested reduced motion.
|
* @returns Whether the user has requested reduced motion.
|
||||||
*/
|
*/
|
||||||
export const usePrefersReducedMotion = () => {
|
export const usePrefersReducedMotion = () =>
|
||||||
const mediaQuery = useRef<MediaQueryList>();
|
useMediaQuery("(prefers-reduced-motion)");
|
||||||
if (mediaQuery.current === undefined)
|
|
||||||
mediaQuery.current = matchMedia("(prefers-reduced-motion)");
|
|
||||||
|
|
||||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState(
|
|
||||||
mediaQuery.current.matches
|
|
||||||
);
|
|
||||||
useEventTarget(
|
|
||||||
mediaQuery.current!,
|
|
||||||
"change",
|
|
||||||
useCallback(
|
|
||||||
() => setPrefersReducedMotion(mediaQuery.current!.matches),
|
|
||||||
[setPrefersReducedMotion]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return prefersReducedMotion;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ limitations under the License.
|
|||||||
|
|
||||||
.bigGrid {
|
.bigGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: 163px;
|
grid-auto-rows: 130px;
|
||||||
gap: 8px;
|
gap: var(--cpd-space-2x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
.bigGrid {
|
.bigGrid {
|
||||||
grid-auto-rows: 183px;
|
grid-auto-rows: 135px;
|
||||||
column-gap: 18px;
|
gap: var(--cpd-space-5x);
|
||||||
row-gap: 21px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -957,7 +957,7 @@ function updateTiles(g: Grid, tiles: TileDescriptor<unknown>[]): Grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateBounds(g: Grid, bounds: RectReadOnly): Grid {
|
function updateBounds(g: Grid, bounds: RectReadOnly): Grid {
|
||||||
const columns = Math.max(2, Math.floor(bounds.width * 0.0045));
|
const columns = Math.max(2, Math.floor(bounds.width * 0.0055));
|
||||||
return columns === g.columns ? g : resize(g, columns);
|
return columns === g.columns ? g : resize(g, columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ limitations under the License.
|
|||||||
contain: strict;
|
contain: strict;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 0 20px var(--footerHeight);
|
margin-inline: var(--inline-content-inset);
|
||||||
|
padding-block-end: var(--footerHeight);
|
||||||
|
margin-block-start: var(--cpd-space-4x);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
@@ -30,9 +32,3 @@ limitations under the License.
|
|||||||
.slot {
|
.slot {
|
||||||
contain: strict;
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.grid {
|
|
||||||
padding: 0 22px var(--footerHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export function NewVideoGrid<T>({
|
|||||||
from: ({ x, y, width, height }: Tile<T>) => ({
|
from: ({ x, y, width, height }: Tile<T>) => ({
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
scale: 0,
|
scale: 0,
|
||||||
shadow: 1,
|
shadow: 0,
|
||||||
shadowSpread: 0,
|
shadowSpread: 0,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
x,
|
x,
|
||||||
@@ -221,7 +221,7 @@ export function NewVideoGrid<T>({
|
|||||||
? {
|
? {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
shadow: 1,
|
shadow: 0,
|
||||||
x: tile.x,
|
x: tile.x,
|
||||||
y: tile.y,
|
y: tile.y,
|
||||||
width: tile.width,
|
width: tile.width,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import useMeasure from "react-use-measure";
|
|||||||
import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver as JuggleResizeObserver } from "@juggle/resize-observer";
|
||||||
|
|
||||||
import styles from "./VideoGrid.module.css";
|
import styles from "./VideoGrid.module.css";
|
||||||
import { Layout } from "../room/GridLayoutMenu";
|
import { Layout } from "../room/LayoutToggle";
|
||||||
import { TileWrapper } from "./TileWrapper";
|
import { TileWrapper } from "./TileWrapper";
|
||||||
import { LayoutStatesMap } from "./Layout";
|
import { LayoutStatesMap } from "./Layout";
|
||||||
|
|
||||||
@@ -81,8 +81,8 @@ export function useVideoGridLayout(hasScreenshareFeeds: boolean): {
|
|||||||
layout: Layout;
|
layout: Layout;
|
||||||
setLayout: (layout: Layout) => void;
|
setLayout: (layout: Layout) => void;
|
||||||
} {
|
} {
|
||||||
const layoutRef = useRef<Layout>("freedom");
|
const layoutRef = useRef<Layout>("grid");
|
||||||
const revertLayoutRef = useRef<Layout>("freedom");
|
const revertLayoutRef = useRef<Layout>("grid");
|
||||||
const prevHasScreenshareFeeds = useRef(hasScreenshareFeeds);
|
const prevHasScreenshareFeeds = useRef(hasScreenshareFeeds);
|
||||||
const [, forceUpdate] = useState({});
|
const [, forceUpdate] = useState({});
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ function getTilePositions(
|
|||||||
pipYRatio: number,
|
pipYRatio: number,
|
||||||
layout: Layout
|
layout: Layout
|
||||||
): TilePosition[] {
|
): TilePosition[] {
|
||||||
if (layout === "freedom") {
|
if (layout === "grid") {
|
||||||
if (tileCount === 2 && focusedTileCount === 0) {
|
if (tileCount === 2 && focusedTileCount === 0) {
|
||||||
return getOneOnOneLayoutTilePositions(
|
return getOneOnOneLayoutTilePositions(
|
||||||
gridWidth,
|
gridWidth,
|
||||||
@@ -700,7 +700,7 @@ function displayedTileCount(
|
|||||||
gridHeight: number
|
gridHeight: number
|
||||||
): number {
|
): number {
|
||||||
let displayedTile = -1;
|
let displayedTile = -1;
|
||||||
if (layout === "freedom") {
|
if (layout === "grid") {
|
||||||
return displayedTile;
|
return displayedTile;
|
||||||
}
|
}
|
||||||
if (tileCount < 2) {
|
if (tileCount < 2) {
|
||||||
@@ -734,7 +734,7 @@ function reorderTiles<T>(tiles: Tile<T>[], layout: Layout, displayedTile = -1) {
|
|||||||
// can assign multiple remote tiles order '1' and this persists through
|
// can assign multiple remote tiles order '1' and this persists through
|
||||||
// subsequent reorders because we preserve the order of the tiles.
|
// subsequent reorders because we preserve the order of the tiles.
|
||||||
if (
|
if (
|
||||||
layout === "freedom" &&
|
layout === "grid" &&
|
||||||
tiles.length === 2 &&
|
tiles.length === 2 &&
|
||||||
tiles.filter((t) => t.item.local).length === 1 &&
|
tiles.filter((t) => t.item.local).length === 1 &&
|
||||||
!tiles.some((t) => t.focused)
|
!tiles.some((t) => t.focused)
|
||||||
@@ -1082,7 +1082,7 @@ export function VideoGrid<T>({
|
|||||||
y?: number;
|
y?: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
} = { shadow: 1, scale: 0, opacity: 0 };
|
} = { shadow: 0, scale: 0, opacity: 0 };
|
||||||
let reset = false;
|
let reset = false;
|
||||||
|
|
||||||
if (!tilePositionsWereValid) {
|
if (!tilePositionsWereValid) {
|
||||||
@@ -1105,7 +1105,7 @@ export function VideoGrid<T>({
|
|||||||
scale: remove ? 0 : 1,
|
scale: remove ? 0 : 1,
|
||||||
opacity: remove ? 0 : 1,
|
opacity: remove ? 0 : 1,
|
||||||
zIndex: tilePosition.zIndex,
|
zIndex: tilePosition.zIndex,
|
||||||
shadow: 1,
|
shadow: oneOnOneLayout && tile.item.local ? 1 : 0,
|
||||||
shadowSpread: oneOnOneLayout && tile.item.local ? 1 : 0,
|
shadowSpread: oneOnOneLayout && tile.item.local ? 1 : 0,
|
||||||
from,
|
from,
|
||||||
reset,
|
reset,
|
||||||
@@ -1143,7 +1143,7 @@ export function VideoGrid<T>({
|
|||||||
lastTappedRef.current[tileKey] = 0;
|
lastTappedRef.current[tileKey] = 0;
|
||||||
|
|
||||||
const tile = tiles.find((tile) => tile.key === tileKey);
|
const tile = tiles.find((tile) => tile.key === tileKey);
|
||||||
if (!tile || layout !== "freedom") return;
|
if (!tile || layout !== "grid") return;
|
||||||
const item = tile.item;
|
const item = tile.item;
|
||||||
|
|
||||||
setTileState(({ tiles, ...state }) => {
|
setTileState(({ tiles, ...state }) => {
|
||||||
@@ -1216,7 +1216,7 @@ export function VideoGrid<T>({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layout !== "freedom") return;
|
if (layout !== "grid") return;
|
||||||
|
|
||||||
const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId);
|
const dragTileIndex = tiles.findIndex((tile) => tile.key === tileId);
|
||||||
const dragTile = tiles[dragTileIndex];
|
const dragTile = tiles[dragTileIndex];
|
||||||
@@ -1392,5 +1392,5 @@ export function VideoGrid<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
VideoGrid.defaultProps = {
|
VideoGrid.defaultProps = {
|
||||||
layout: "freedom",
|
layout: "grid",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,14 +20,11 @@ limitations under the License.
|
|||||||
top: 0;
|
top: 0;
|
||||||
container-name: videoTile;
|
container-name: videoTile;
|
||||||
container-type: size;
|
container-type: size;
|
||||||
--tileRadius: 8px;
|
border-radius: var(--cpd-space-4x);
|
||||||
border-radius: var(--tileRadius);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
outline: 2px solid rgba(0, 0, 0, 0);
|
||||||
/* HACK: This has no visual effect due to the short duration, but allows the
|
transition: outline-radius ease 0.15s, outline-color ease 0.15s;
|
||||||
JS to detect movement via the transform property for audio spatialization */
|
|
||||||
transition: transform 0.000000001s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoTile * {
|
.videoTile * {
|
||||||
@@ -45,21 +42,14 @@ limitations under the License.
|
|||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoTile::after {
|
.videoTile.speaking {
|
||||||
position: absolute;
|
outline: 4px solid var(--cpd-color-border-accent);
|
||||||
top: -1px;
|
|
||||||
left: -1px;
|
|
||||||
right: -1px;
|
|
||||||
bottom: -1px;
|
|
||||||
content: "";
|
|
||||||
border-radius: var(--tileRadius);
|
|
||||||
box-shadow: inset 0 0 0 4px var(--cpd-color-border-accent) !important;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity ease 0.15s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoTile.speaking::after {
|
@media (hover: hover) {
|
||||||
opacity: 1;
|
.videoTile:hover {
|
||||||
|
outline: 2px solid var(--cpd-color-gray-1400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoTile.maximised {
|
.videoTile.maximised {
|
||||||
@@ -73,30 +63,47 @@ limitations under the License.
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoBubble {
|
.nameTag {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 24px;
|
inset-inline-start: var(--cpd-space-1x);
|
||||||
padding: 0 8px;
|
inset-block-end: var(--cpd-space-1x);
|
||||||
|
padding: var(--cpd-space-1x);
|
||||||
|
padding-block: var(--cpd-space-1x);
|
||||||
color: var(--cpd-color-text-primary);
|
color: var(--cpd-color-text-primary);
|
||||||
background-color: var(--stopgap-background-85);
|
/* TODO: un-hardcode this color. It comes from the dark theme. */
|
||||||
|
background-color: rgba(237, 244, 252, 0.79);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
border-radius: var(--cpd-radius-pill-effect);
|
||||||
border-radius: 4px;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
max-width: calc(100% - 48px);
|
max-width: calc(100% - 48px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
box-shadow: var(--small-drop-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoBubble > svg {
|
:global(.cpd-theme-dark) .nameTag {
|
||||||
height: 16px;
|
/* TODO: un-hardcode this color. It comes from the light theme. */
|
||||||
width: 16px;
|
background-color: rgba(2, 7, 13, 0.77);
|
||||||
margin-right: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoBubble > svg * {
|
.nameTag > svg {
|
||||||
fill: var(--cpd-color-icon-primary);
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameTag > svg[data-muted="true"] {
|
||||||
|
color: var(--cpd-color-icon-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameTag > svg[data-muted="false"] {
|
||||||
|
color: var(--cpd-color-icon-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameTag span {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-inline: var(--cpd-space-2x);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -137,24 +144,6 @@ limitations under the License.
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memberName {
|
|
||||||
left: 16px;
|
|
||||||
bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.memberName > :last-child {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.memberName span {
|
|
||||||
font-size: var(--font-size-caption);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: var(--font-size-body);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoMutedOverlay {
|
.videoMutedOverlay {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -192,12 +181,6 @@ limitations under the License.
|
|||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
.videoTile {
|
|
||||||
--tileRadius: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CSS makes us put a condition here, even though all we want to do is
|
/* CSS makes us put a condition here, even though all we want to do is
|
||||||
unconditionally select the container so we can use cqmin units */
|
unconditionally select the container so we can use cqmin units */
|
||||||
@container videoTile (width > 0) {
|
@container videoTile (width > 0) {
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ import {
|
|||||||
RoomMember,
|
RoomMember,
|
||||||
RoomMemberEvent,
|
RoomMemberEvent,
|
||||||
} from "matrix-js-sdk/src/models/room-member";
|
} from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { ReactComponent as MicOnSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-on-solid.svg";
|
||||||
|
import { ReactComponent as MicOffSolidIcon } from "@vector-im/compound-design-tokens/icons/mic-off-solid.svg";
|
||||||
|
import { Text } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
import styles from "./VideoTile.module.css";
|
import styles from "./VideoTile.module.css";
|
||||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
|
||||||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
|
||||||
import { useReactiveState } from "../useReactiveState";
|
import { useReactiveState } from "../useReactiveState";
|
||||||
import { AudioButton, FullscreenButton } from "../button/Button";
|
import { AudioButton, FullscreenButton } from "../button/Button";
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
@@ -102,12 +103,15 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
}
|
}
|
||||||
}, [member, setDisplayName]);
|
}, [member, setDisplayName]);
|
||||||
|
|
||||||
const { isMuted: microphoneMuted } = useMediaTrack(
|
const muted =
|
||||||
content === TileContent.UserMedia
|
useMediaTrack(
|
||||||
? Track.Source.Microphone
|
content === TileContent.UserMedia
|
||||||
: Track.Source.ScreenShareAudio,
|
? Track.Source.Microphone
|
||||||
sfuParticipant
|
: Track.Source.ScreenShareAudio,
|
||||||
);
|
sfuParticipant
|
||||||
|
).isMuted !== false;
|
||||||
|
|
||||||
|
const MicIcon = muted ? MicOffSolidIcon : MicOnSolidIcon;
|
||||||
|
|
||||||
const onFullscreen = useCallback(() => {
|
const onFullscreen = useCallback(() => {
|
||||||
onToggleFullscreen(data.id);
|
onToggleFullscreen(data.id);
|
||||||
@@ -153,7 +157,6 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
sfuParticipant.isSpeaking &&
|
sfuParticipant.isSpeaking &&
|
||||||
content === TileContent.UserMedia &&
|
content === TileContent.UserMedia &&
|
||||||
showSpeakingIndicator,
|
showSpeakingIndicator,
|
||||||
[styles.muted]: microphoneMuted,
|
|
||||||
[styles.screenshare]: content === TileContent.ScreenShare,
|
[styles.screenshare]: content === TileContent.ScreenShare,
|
||||||
[styles.maximised]: maximised,
|
[styles.maximised]: maximised,
|
||||||
})}
|
})}
|
||||||
@@ -169,9 +172,10 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
<div className={styles.videoMutedOverlay} />
|
<div className={styles.videoMutedOverlay} />
|
||||||
<Avatar
|
<Avatar
|
||||||
key={member?.userId}
|
key={member?.userId}
|
||||||
|
id={member?.userId ?? displayName}
|
||||||
|
name={displayName}
|
||||||
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
|
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
|
||||||
src={member?.getMxcAvatarUrl()}
|
src={member?.getMxcAvatarUrl()}
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -181,11 +185,17 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
<span>{t("{{displayName}} is presenting", { displayName })}</span>
|
<span>{t("{{displayName}} is presenting", { displayName })}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
<div className={styles.nameTag}>
|
||||||
{microphoneMuted === false ? <MicIcon /> : <MicMutedIcon />}
|
<MicIcon
|
||||||
<span data-testid="videoTile_caption" title={displayName}>
|
width={20}
|
||||||
{displayName}
|
height={20}
|
||||||
</span>
|
aria-label={muted ? t("Microphone off") : t("Microphone on")}
|
||||||
|
data-muted={muted}
|
||||||
|
/>
|
||||||
|
<Text as="span" size="sm" weight="medium" data-testid="videoTile_caption">
|
||||||
|
{sfuParticipant.isLocal ? t("You") : displayName}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{showConnectionStats && (
|
{showConnectionStats && (
|
||||||
<ConnectionQualityIndicator participant={sfuParticipant} />
|
<ConnectionQualityIndicator participant={sfuParticipant} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export const widget: WidgetHelpers | null = (() => {
|
|||||||
receiveState,
|
receiveState,
|
||||||
sendToDevice: sendRecvToDevice,
|
sendToDevice: sendRecvToDevice,
|
||||||
receiveToDevice: sendRecvToDevice,
|
receiveToDevice: sendRecvToDevice,
|
||||||
turnServers: true,
|
turnServers: false,
|
||||||
},
|
},
|
||||||
roomId,
|
roomId,
|
||||||
{
|
{
|
||||||
@@ -157,15 +157,6 @@ export const widget: WidgetHelpers | null = (() => {
|
|||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
useE2eForGroupCall: e2eEnabled,
|
useE2eForGroupCall: e2eEnabled,
|
||||||
fallbackICEServerAllowed: allowIceFallback,
|
fallbackICEServerAllowed: allowIceFallback,
|
||||||
// XXX: The client expects the list of foci in its constructor, but we don't
|
|
||||||
// know this until we fetch the config file. However, we can't wait to construct
|
|
||||||
// the client object or we'll miss the 'capabilities' request from the host app.
|
|
||||||
// As of writing this, I have made the embedded widget client send the 'contentLoaded'
|
|
||||||
// message so that we can use the widget API in less racy mode, but we need to change
|
|
||||||
// element-web to use waitForIFrameLoad=false. Once that change has rolled out,
|
|
||||||
// we can just start the client after we've fetched the config.
|
|
||||||
livekitServiceURL: undefined,
|
|
||||||
useLivekitForGroupCalls: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -174,13 +165,6 @@ export const widget: WidgetHelpers | null = (() => {
|
|||||||
// wait for the config file to be ready (we load very early on so it might not
|
// wait for the config file to be ready (we load very early on so it might not
|
||||||
// be otherwise)
|
// be otherwise)
|
||||||
await Config.init();
|
await Config.init();
|
||||||
const livekit = Config.get().livekit;
|
|
||||||
const focus = livekit?.livekit_service_url;
|
|
||||||
// Now we've fetched the config, be evil and use the getter to inject the focus
|
|
||||||
// into the client (see above XXX).
|
|
||||||
if (focus) {
|
|
||||||
client.setLivekitServiceURL(livekit.livekit_service_url);
|
|
||||||
}
|
|
||||||
await client.startClient();
|
await client.startClient();
|
||||||
resolve(client);
|
resolve(client);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ export default defineConfig(({ mode }) => {
|
|||||||
const plugins = [
|
const plugins = [
|
||||||
react(),
|
react(),
|
||||||
basicSsl(),
|
basicSsl(),
|
||||||
svgrPlugin(),
|
svgrPlugin({
|
||||||
|
svgrOptions: {
|
||||||
|
// This enables ref forwarding on SVGR components, which is needed, for
|
||||||
|
// example, to make tooltips on icons work
|
||||||
|
ref: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
htmlTemplate.default({
|
htmlTemplate.default({
|
||||||
data: {
|
data: {
|
||||||
title: env.VITE_PRODUCT_NAME || "Element Call",
|
title: env.VITE_PRODUCT_NAME || "Element Call",
|
||||||
|
|||||||