Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54fe2aa7a3 | ||
|
|
3ff201562b | ||
|
|
e139ac6584 | ||
|
|
85210df28e | ||
|
|
0af116ce76 | ||
|
|
a09bb109fd | ||
|
|
c97185a50e | ||
|
|
50f7fedfa0 | ||
|
|
178c6496bd | ||
|
|
c5eb9f0b99 | ||
|
|
af4c1280f5 | ||
|
|
97ae11f656 | ||
|
|
e182dd50f2 | ||
|
|
43f98e6be6 | ||
|
|
70ba6c3c6b | ||
|
|
29a7376bc7 | ||
|
|
db02178fce | ||
|
|
1d69bef7f9 | ||
|
|
0a83a8804f | ||
|
|
5795e20865 | ||
|
|
4aba1c8b74 | ||
|
|
dc694d4ffe | ||
|
|
fafc56bb90 | ||
|
|
a83611c287 | ||
|
|
2cca320291 | ||
|
|
834582a870 | ||
|
|
2390b990c5 | ||
|
|
166046a4b1 | ||
|
|
f2dbe8abbe | ||
|
|
1a814713df | ||
|
|
fceb10e2df | ||
|
|
94323b3597 | ||
|
|
a8c5cb4821 | ||
|
|
6e32aad729 | ||
|
|
49f6249144 | ||
|
|
ab08b58ef5 | ||
|
|
ba9efc64c3 | ||
|
|
e986ef914f | ||
|
|
68117cd9e4 | ||
|
|
ccb4f8c0e4 | ||
|
|
1367a50b75 | ||
|
|
aec21e661d | ||
|
|
ae7697b33c | ||
|
|
37f72fe0b6 | ||
|
|
5660938f47 | ||
|
|
1c76385d79 | ||
|
|
208a3d9045 | ||
|
|
16c9483f37 | ||
|
|
70939fa8f0 | ||
|
|
ec1f846c92 | ||
|
|
1570657176 | ||
|
|
7e78f7a670 | ||
|
|
d556fe188a | ||
|
|
c07aeb3ba8 | ||
|
|
a6c6aed61c | ||
|
|
28a20d9b1e | ||
|
|
077e361a26 | ||
|
|
6180f2e1b9 | ||
|
|
5e57a56d21 | ||
|
|
402f62e09a | ||
|
|
6ec2e9c822 | ||
|
|
684defdc19 | ||
|
|
5ed2dc6e0e | ||
|
|
ce86a6f120 | ||
|
|
96b1a5f296 | ||
|
|
e9ebccf0df | ||
|
|
02b2aef958 | ||
|
|
c6d60cff64 | ||
|
|
81771f511c | ||
|
|
004160b664 | ||
|
|
2d25d3c2bc | ||
|
|
4728804a33 | ||
|
|
8524b9ecd6 | ||
|
|
eca598e28f | ||
|
|
f808c56121 | ||
|
|
77da0c912f | ||
|
|
e8a875eb32 | ||
|
|
e7a94426c2 | ||
|
|
17613837b6 | ||
|
|
4b4c98066c | ||
|
|
4a5b69800c | ||
|
|
70d6c3e9bf | ||
|
|
90e32af220 | ||
|
|
fdc0272940 | ||
|
|
d90a837714 |
20
i18next-parser.config.js
Normal file
20
i18next-parser.config.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
keySeparator: false,
|
||||||
|
namespaceSeparator: false,
|
||||||
|
contextSeparator: "|",
|
||||||
|
pluralSeparator: "|",
|
||||||
|
createOldCatalogs: false,
|
||||||
|
defaultNamespace: "app",
|
||||||
|
lexers: {
|
||||||
|
ts: [{
|
||||||
|
lexer: "JavascriptLexer",
|
||||||
|
functions: ["t", "translatedError"],
|
||||||
|
functionsNamespace: ["useTranslation", "withTranslation"],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
locales: ["en-GB"],
|
||||||
|
output: "public/locales/$LOCALE/$NAMESPACE.json",
|
||||||
|
input: ["src/**/*.{ts,tsx}"],
|
||||||
|
sort: true,
|
||||||
|
useKeysAsDefaultValue: true,
|
||||||
|
};
|
||||||
11
package.json
11
package.json
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -10,7 +11,8 @@
|
|||||||
"prettier:format": "prettier -w src",
|
"prettier:format": "prettier -w src",
|
||||||
"lint": "yarn lint:types && yarn lint:js",
|
"lint": "yarn lint:types && yarn lint:js",
|
||||||
"lint:js": "eslint --max-warnings 0 src",
|
"lint:js": "eslint --max-warnings 0 src",
|
||||||
"lint:types": "tsc"
|
"lint:types": "tsc",
|
||||||
|
"i18n": "node_modules/i18next-parser/bin/cli.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
@@ -38,7 +40,10 @@
|
|||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"color-hash": "^2.0.1",
|
"color-hash": "^2.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f41b7706e489cc1b83e6b25dd50091be2b5a9083",
|
"i18next": "^21.10.0",
|
||||||
|
"i18next-browser-languagedetector": "^6.1.8",
|
||||||
|
"i18next-http-backend": "^1.4.4",
|
||||||
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#5a0787349d4951012eabe72f3363c17bdcda0d56",
|
||||||
"matrix-widget-api": "^1.0.0",
|
"matrix-widget-api": "^1.0.0",
|
||||||
"mermaid": "^8.13.8",
|
"mermaid": "^8.13.8",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
@@ -47,6 +52,7 @@
|
|||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
"react-dom": "^17.0.0",
|
"react-dom": "^17.0.0",
|
||||||
|
"react-i18next": "^11.18.6",
|
||||||
"react-json-view": "^1.21.3",
|
"react-json-view": "^1.21.3",
|
||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
@@ -71,6 +77,7 @@
|
|||||||
"eslint-plugin-matrix-org": "^0.4.0",
|
"eslint-plugin-matrix-org": "^0.4.0",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-react-hooks": "^4.5.0",
|
"eslint-plugin-react-hooks": "^4.5.0",
|
||||||
|
"i18next-parser": "^6.6.0",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
"storybook-builder-vite": "^0.1.12",
|
"storybook-builder-vite": "^0.1.12",
|
||||||
|
|||||||
132
public/locales/bg/app.json
Normal file
132
public/locales/bg/app.json
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"<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>Create an account</0> Or <2>Access as a guest</2>": "<0>Създайте акаунт</0> или <2>Влезте като гост</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Възникна грешка.</0><1>Изпращнето на debug логове ще ни помогне да открием проблема.</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>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Приемете разрешенията за камера/микрофон за да се присъедините в разговора.",
|
||||||
|
"Accept microphone permissions to join the call.": "Приемете разрешението за микрофона за да се присъедините в разговора.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Друг потребител в този разговор има проблем. За да диагностицираме този проблем по-добре ни се иска да съберем debug логове.",
|
||||||
|
"Audio": "Звук",
|
||||||
|
"Avatar": "Аватар",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Напред\" се съгласявате с нашите <2>Правила и условия</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Натискайки \"Влез в разговора сега\", се съгласявате с нашите <2>Правила и условия</2>",
|
||||||
|
"Call link copied": "Връзка към разговора бе копирана",
|
||||||
|
"Call type menu": "Меню \"тип на разговора\"",
|
||||||
|
"Camera": "Камера",
|
||||||
|
"Camera {{n}}": "Камера {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Необходими са разрешения за камера/микрофон за да се присъедините в разговора.",
|
||||||
|
"Change layout": "Промени изгледа",
|
||||||
|
"Close": "Затвори",
|
||||||
|
"Confirm password": "Потвърди паролата",
|
||||||
|
"Connection lost": "Връзката се изгуби",
|
||||||
|
"Copied!": "Копирано!",
|
||||||
|
"Copy and share this call link": "Копирай и сподели връзка към разговора",
|
||||||
|
"Copy call link and join later": "Копирай връзка към разговора и се присъедини по-късно",
|
||||||
|
"Create account": "Създай акаунт",
|
||||||
|
"Debug log": "Debug логове",
|
||||||
|
"Debug log request": "Заявка за debug логове",
|
||||||
|
"Description (optional)": "Описание (незадължително)",
|
||||||
|
"Details": "Детайли",
|
||||||
|
"Developer": "Разработчик",
|
||||||
|
"Display name": "Име/псевдоним",
|
||||||
|
"Download debug logs": "Изтеглете debug логове",
|
||||||
|
"Entering room…": "Влизане в стаята…",
|
||||||
|
"Exit full screen": "Излез от цял екран",
|
||||||
|
"Fetching group call timed out.": "Изтече времето за взимане на груповия разговор.",
|
||||||
|
"Freedom": "Свобода",
|
||||||
|
"Full screen": "Цял екран",
|
||||||
|
"Go": "Напред",
|
||||||
|
"Grid layout menu": "Меню \"решетков изглед\"",
|
||||||
|
"Having trouble? Help us fix it.": "Имате проблем? Помогнете да го поправим.",
|
||||||
|
"Home": "Начало",
|
||||||
|
"Include debug logs": "Включи debug логове",
|
||||||
|
"Incompatible versions": "Несъвместими версии",
|
||||||
|
"Incompatible versions!": "Несъвместими версии!",
|
||||||
|
"Inspector": "Инспектор",
|
||||||
|
"Invite": "Покани",
|
||||||
|
"Invite people": "Покани хора",
|
||||||
|
"Join call": "Влез в разговора",
|
||||||
|
"Join call now": "Влез в разговора сега",
|
||||||
|
"Join existing call?": "Присъединяване към съществуващ разговор?",
|
||||||
|
"Leave": "Напусни",
|
||||||
|
"Loading room…": "Напускане на стаята…",
|
||||||
|
"Loading…": "Зареждане…",
|
||||||
|
"Local volume": "Локална сила на звука",
|
||||||
|
"Logging in…": "Влизане…",
|
||||||
|
"Login": "Влез",
|
||||||
|
"Login to your account": "Влезте в акаунта си",
|
||||||
|
"Microphone": "Микрофон",
|
||||||
|
"Microphone permissions needed to join the call.": "Необходими са разрешения за микрофона за да можете да се присъедините в разговора.",
|
||||||
|
"Microphone {{n}}": "Микрофон {{n}}",
|
||||||
|
"More": "Още",
|
||||||
|
"More menu": "Мено \"още\"",
|
||||||
|
"Mute microphone": "Заглуши микрофона",
|
||||||
|
"No": "Не",
|
||||||
|
"Not now, return to home screen": "Не сега, върни се на началния екран",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Все още не сте регистрирани? <2>Създайте акаунт</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>": "Други потребители се опитват да се присъединят в разговора от несъвместими версии. Следните потребители трябва да проверят дали са презаредили браузърите си<1>{userLis}</1>",
|
||||||
|
"Password": "Парола",
|
||||||
|
"Passwords must match": "Паролите не съвпадат",
|
||||||
|
"Press and hold spacebar to talk": "Натиснете и задръжте Space за да говорите",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Натиснете и задръжте Space за да говорите заедно с {{name}}",
|
||||||
|
"Press and hold to talk": "Натиснете и задръжте за да говорите",
|
||||||
|
"Press and hold to talk over {{name}}": "Натиснете и задръжте за да говорите заедно с {{name}}",
|
||||||
|
"Profile": "Профил",
|
||||||
|
"Recaptcha dismissed": "Recaptcha отхвърлена",
|
||||||
|
"Recaptcha not loaded": "Recaptcha не е заредена",
|
||||||
|
"Register": "Регистрация",
|
||||||
|
"Registering…": "Регистриране…",
|
||||||
|
"Release spacebar key to stop": "Отпуснете Space за да спрете",
|
||||||
|
"Release to stop": "Отпуснете за да спрете",
|
||||||
|
"Remove": "Премахни",
|
||||||
|
"Return to home screen": "Връщане на началния екран",
|
||||||
|
"Save": "Запази",
|
||||||
|
"Saving…": "Запазване…",
|
||||||
|
"Select an option": "Изберете опция",
|
||||||
|
"Send debug logs": "Изпратете debug логове",
|
||||||
|
"Sending…": "Изпращане…",
|
||||||
|
"Settings": "Настройки",
|
||||||
|
"Share screen": "Сподели екрана",
|
||||||
|
"Show call inspector": "Покажи инспектора на разговора",
|
||||||
|
"Sign in": "Влез",
|
||||||
|
"Sign out": "Излез",
|
||||||
|
"Spatial audio": "Пространствен звук",
|
||||||
|
"Speaker": "Говорител",
|
||||||
|
"Speaker {{n}}": "Говорител {{n}}",
|
||||||
|
"Spotlight": "Прожектор",
|
||||||
|
"Stop sharing screen": "Спри споделянето на екрана",
|
||||||
|
"Submit feedback": "Изпрати обратна връзка",
|
||||||
|
"Submitting feedback…": "Изпращане на обратна връзка…",
|
||||||
|
"Take me Home": "Отиди в Начало",
|
||||||
|
"Talk over speaker": "Говорете заедно с говорителя",
|
||||||
|
"Talking…": "Говорене…",
|
||||||
|
"Thanks! We'll get right on it.": "Благодарим! Веднага ще се заемем.",
|
||||||
|
"This call already exists, would you like to join?": "Този разговор вече съществува, искате ли да се присъедините?",
|
||||||
|
"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>Terms and conditions</12>": "Този сайт се предпазва от ReCAPTCHA и важат <2>Политиката за поверителност</2> и <6>Условията за ползване на услугата</6> на Google.<9></9>Натискайки \"Регистрация\", се съгласявате с нашите <12>Правила и условия</12>",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Това прави звука на говорителя да изглежда, че излиза от мястото където са позиционирани на екрана. (Експериментална функция: може да повлияе на стабилността на звука.)",
|
||||||
|
"Turn off camera": "Изключи камерата",
|
||||||
|
"Turn on camera": "Включи камерата",
|
||||||
|
"Unmute microphone": "Включи микрофона",
|
||||||
|
"User ID": "Потребителски идентификатор",
|
||||||
|
"User menu": "Потребителско меню",
|
||||||
|
"Username": "Потребителско име",
|
||||||
|
"Version: {{version}}": "Версия: {{version}}",
|
||||||
|
"Video": "Видео",
|
||||||
|
"Video call": "Видео разговор",
|
||||||
|
"Video call name": "Име на видео разговора",
|
||||||
|
"Waiting for network": "Изчакване на мрежата",
|
||||||
|
"Waiting for other participants…": "Изчакване на други участници…",
|
||||||
|
"Walkie-talkie call": "Уоки-токи разговор",
|
||||||
|
"Walkie-talkie call name": "Име на уоки-токи разговора",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не се поддържа или се блокира от браузъра.",
|
||||||
|
"Yes, join call": "Да, присъедини се",
|
||||||
|
"You can't talk at the same time": "Не можете да говорите едновременно",
|
||||||
|
"Your recent calls": "Скорошните ви разговори",
|
||||||
|
"{{count}} people connected|one": "{{count}} човек се свърза",
|
||||||
|
"{{count}} people connected|other": "{{count}} човека се звързаха",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, разговорът ви приключи",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is presenting": "{{name}} презентира",
|
||||||
|
"{{name}} is talking…": "{{name}} говори…",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - уоки-токи разговор"
|
||||||
|
}
|
||||||
133
public/locales/de/app.json
Normal file
133
public/locales/de/app.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Du hast bereits ein Konto?</0><1><0>Anmelden</0> Oder <2>Als Gast betreten</2></1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Konto erstellen</0> Oder <2>Als Gast betreten</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Hoppla, da ist etwas schief gelaufen.</0><1>Die Übermittlung von Debug-Protokollen wird uns helfen, das Problem zu finden.</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>Warum vergibst Du nicht abschließend ein Passwort, um Dein Konto zu erhalten?</0><1>Du kannst Deinen Namen behalten und einen Avatar für zukünftige Anrufe festlegen.</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Erlaube Zugriff auf Kamera/Mikrofon um dem Anruf beizutreten.",
|
||||||
|
"Accept microphone permissions to join the call.": "Erlaube Zugriff auf das Mikrofon um dem Anruf beizutreten.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Ein anderer Benutzer dieses Anrufs hat ein Problem. Um dieses besser diagnostizieren zu können, würden wir gerne ein Debug-Protokoll erstellen.",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Mit dem Klick auf \"Los geht's\", akzeptierst Du unsere <2>Geschäftsbedingungen</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Wenn Du auf \"Jetzt anrufen\" klickst, erklärst Du dich mit unserer <2>Geschäftsbedingungen</2> einverstanden",
|
||||||
|
"Call link copied": "Anruflink kopiert",
|
||||||
|
"Call type menu": "Anruftyp Menü",
|
||||||
|
"Camera": "Kamera",
|
||||||
|
"Camera {{n}}": "Kamera {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Kamera-/Mikrofonberechtigung für die Teilnahme am Anruf erforderlich.",
|
||||||
|
"Change layout": "Layout ändern",
|
||||||
|
"Close": "Schließen",
|
||||||
|
"Confirm password": "Passwort bestätigen",
|
||||||
|
"Connection lost": "Verbindung verloren",
|
||||||
|
"Copied!": "Kopiert!",
|
||||||
|
"Copy and share this call link": "Kopiere und teile diesen Anruflink",
|
||||||
|
"Copy call link and join later": "Kopiere den Anruflink und nehme später teil",
|
||||||
|
"Create account": "Konto erstellen",
|
||||||
|
"Debug log": "Debug-Protokoll",
|
||||||
|
"Debug log request": "Debug-Log Anfrage",
|
||||||
|
"Description (optional)": "Beschreibung (wahlweise)",
|
||||||
|
"Details": "Details",
|
||||||
|
"Developer": "Entwickler",
|
||||||
|
"Display name": "Anzeigename",
|
||||||
|
"Download debug logs": "Debug-Logs herunterladen",
|
||||||
|
"Entering room…": "Betrete Raum …",
|
||||||
|
"Exit full screen": "Vollbildmodus verlassen",
|
||||||
|
"Freedom": "Freiraum",
|
||||||
|
"Full screen": "Vollbild",
|
||||||
|
"Go": "Los geht's",
|
||||||
|
"Grid layout menu": "Grid-Layout-Menü",
|
||||||
|
"Having trouble? Help us fix it.": "Hast Du Probleme? Hilf uns, es zu beheben.",
|
||||||
|
"Home": "Startseite",
|
||||||
|
"Include debug logs": "Debug-Logs hinzufügen",
|
||||||
|
"Incompatible versions": "Inkompatible Versionen",
|
||||||
|
"Incompatible versions!": "Inkompatible Versionen!",
|
||||||
|
"Inspector": "Inspektor",
|
||||||
|
"Invite": "Einladen",
|
||||||
|
"Invite people": "Personen einladen",
|
||||||
|
"Join call": "Anruf beitreten",
|
||||||
|
"Join call now": "Trete dem Anruf bei",
|
||||||
|
"Join existing call?": "An bestehendem Anruf teilnehmen?",
|
||||||
|
"Leave": "Verlassen",
|
||||||
|
"Loading room…": "Lade Raum …",
|
||||||
|
"Loading…": "Lade …",
|
||||||
|
"Local volume": "Lokale Lautstärke",
|
||||||
|
"Logging in…": "Anmelden …",
|
||||||
|
"Login": "Anmelden",
|
||||||
|
"Login to your account": "Anmeldung bei Deinem Konto",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Microphone permissions needed to join the call.": "Mikrofon Berechtigung ist erforderlich, um dem Anruf beizutreten.",
|
||||||
|
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||||
|
"More": "Mehr",
|
||||||
|
"More menu": "Weiteres Menü",
|
||||||
|
"Mute microphone": "Mikrofon stummschalten",
|
||||||
|
"No": "Nein",
|
||||||
|
"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>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Andere Benutzer versuchen, diesem Aufruf von einer inkompatiblen Softwareversion aus beizutreten. Diese Benutzer sollten ihre Web-Browser Seite neu laden:<1>{userLis}</1>",
|
||||||
|
"Password": "Passwort",
|
||||||
|
"Passwords must match": "Passwörter müssen übereinstimmen",
|
||||||
|
"Press and hold spacebar to talk": "Zum Sprechen die Leertaste gedrückt halten",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen die Leertaste gedrückt halten",
|
||||||
|
"Press and hold to talk": "Zum Sprechen gedrückt halten",
|
||||||
|
"Press and hold to talk over {{name}}": "Zum Verdrängen von {{name}} und Sprechen gedrückt halten",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Recaptcha dismissed": "Recaptcha abgelehnt",
|
||||||
|
"Recaptcha not loaded": "Recaptcha nicht geladen",
|
||||||
|
"Register": "Registrieren",
|
||||||
|
"Registering…": "Registrierung …",
|
||||||
|
"Release spacebar key to stop": "Leertaste loslassen, um zu stoppen",
|
||||||
|
"Release to stop": "Loslassen zum Stoppen",
|
||||||
|
"Remove": "Entfernen",
|
||||||
|
"Return to home screen": "Zurück zum Startbildschirm",
|
||||||
|
"Save": "Speichern",
|
||||||
|
"Saving…": "Speichere …",
|
||||||
|
"Select an option": "Wähle eine Option",
|
||||||
|
"Send debug logs": "Debug-Logs senden",
|
||||||
|
"Sending…": "Senden …",
|
||||||
|
"Settings": "Einstellungen",
|
||||||
|
"Share screen": "Bildschirm teilen",
|
||||||
|
"Show call inspector": "Anrufinspektor anzeigen",
|
||||||
|
"Sign in": "Anmelden",
|
||||||
|
"Sign out": "Abmelden",
|
||||||
|
"Spatial audio": "Räumliche Audiowiedergabe",
|
||||||
|
"Speaker": "Wiedergabegerät",
|
||||||
|
"Speaker {{n}}": "Wiedergabegerät {{n}}",
|
||||||
|
"Spotlight": "Rampenlicht",
|
||||||
|
"Stop sharing screen": "Beenden der Bildschirmfreigabe",
|
||||||
|
"Submit feedback": "Feedback senden",
|
||||||
|
"Submitting feedback…": "Feedback senden …",
|
||||||
|
"Take me Home": "Zurück zur Startseite",
|
||||||
|
"Talk over speaker": "Aktiven Sprecher verdrängen und sprechen",
|
||||||
|
"Talking…": "Sprechen …",
|
||||||
|
"Thanks! We'll get right on it.": "Vielen Dank! Wir werden uns sofort darum kümmern.",
|
||||||
|
"This call already exists, would you like to join?": "Dieser Aufruf existiert bereits, möchtest Du teilnehmen?",
|
||||||
|
"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>Terms and conditions</12>": "Diese Website ist durch ReCAPTCHA geschützt und es gelten die <2>Datenschutzerklärung</2> sowie die <6> Nutzungsbedingungen </6> von Google.<9></9>Indem Du auf \"Registrieren\" klickst, stimmst Du unseren <12>Geschäftsbedingungen</12> zu",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Dadurch wird die Audiowiedergabe eines Sprechers so wiedergegeben, als käme er von der Stelle, an der das zugehörige Videobild auf dem Bildschirm positioniert ist (Experimentelle Funktion: Dies kann die Stabilität der Audiowiedergabe beeinträchtigen).",
|
||||||
|
"Turn off camera": "Kamera ausschalten",
|
||||||
|
"Turn on camera": "Kamera einschalten",
|
||||||
|
"Unmute microphone": "Mikrofon aktivieren",
|
||||||
|
"User ID": "Benutzer ID",
|
||||||
|
"User menu": "Benutzermenü",
|
||||||
|
"Username": "Benutzername",
|
||||||
|
"Version: {{version}}": "Version: {{version}}",
|
||||||
|
"Video": "Video",
|
||||||
|
"Video call": "Videoanruf",
|
||||||
|
"Video call name": "Name des Videoanrufs",
|
||||||
|
"Waiting for network": "Warte auf Netzwerk",
|
||||||
|
"Waiting for other participants…": "Warte auf weitere Teilnehmer …",
|
||||||
|
"Walkie-talkie call": "Walkie-talkie Anruf",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC wird in diesem Web-Browser nicht unterstützt oder ist blockiert.",
|
||||||
|
"Yes, join call": "Ja, Anruf beitreten",
|
||||||
|
"You can't talk at the same time": "Du kannst nicht gleichzeitig sprechen",
|
||||||
|
"Your recent calls": "Deine lezten Anrufe",
|
||||||
|
"{{count}} people connected|one": "{{count}} Teilnehmer verbunden",
|
||||||
|
"{{count}} people connected|other": "{{count}} Teilnehmer verbunden",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, Dein Anruf wurde beendet",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is presenting": "{{name}} präsentiert",
|
||||||
|
"{{name}} is talking…": "{{name}} spricht …",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} – Walkie-Talkie-Anruf",
|
||||||
|
"Fetching group call timed out.": "Zeitüberschreitung beim Abrufen des Gruppenanrufs.",
|
||||||
|
"Walkie-talkie call name": "Walkie-talkie Anruf Name",
|
||||||
|
"Sending debug logs…": "Sende Debug-Logs …"
|
||||||
|
}
|
||||||
133
public/locales/en-GB/app.json
Normal file
133
public/locales/en-GB/app.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"{{count}} people connected|one": "{{count}} person connected",
|
||||||
|
"{{count}} people connected|other": "{{count}} people connected",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, your call is now ended",
|
||||||
|
"{{name}} is presenting": "{{name}} is presenting",
|
||||||
|
"{{name}} is talking…": "{{name}} is talking…",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie 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>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</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>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>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Accept camera/microphone permissions to join the call.",
|
||||||
|
"Accept microphone permissions to join the call.": "Accept microphone permissions to join the call.",
|
||||||
|
"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.",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "By clicking \"Go\", you agree to our <2>Terms and conditions</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>",
|
||||||
|
"Call link copied": "Call link copied",
|
||||||
|
"Call type menu": "Call type menu",
|
||||||
|
"Camera": "Camera",
|
||||||
|
"Camera {{n}}": "Camera {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Camera/microphone permissions needed to join the call.",
|
||||||
|
"Change layout": "Change layout",
|
||||||
|
"Close": "Close",
|
||||||
|
"Confirm password": "Confirm password",
|
||||||
|
"Connection lost": "Connection lost",
|
||||||
|
"Copied!": "Copied!",
|
||||||
|
"Copy and share this call link": "Copy and share this call link",
|
||||||
|
"Copy call link and join later": "Copy call link and join later",
|
||||||
|
"Create account": "Create account",
|
||||||
|
"Debug log": "Debug log",
|
||||||
|
"Debug log request": "Debug log request",
|
||||||
|
"Description (optional)": "Description (optional)",
|
||||||
|
"Details": "Details",
|
||||||
|
"Developer": "Developer",
|
||||||
|
"Display name": "Display name",
|
||||||
|
"Download debug logs": "Download debug logs",
|
||||||
|
"Entering room…": "Entering room…",
|
||||||
|
"Exit full screen": "Exit full screen",
|
||||||
|
"Fetching group call timed out.": "Fetching group call timed out.",
|
||||||
|
"Freedom": "Freedom",
|
||||||
|
"Full screen": "Full screen",
|
||||||
|
"Go": "Go",
|
||||||
|
"Grid layout menu": "Grid layout menu",
|
||||||
|
"Having trouble? Help us fix it.": "Having trouble? Help us fix it.",
|
||||||
|
"Home": "Home",
|
||||||
|
"Include debug logs": "Include debug logs",
|
||||||
|
"Incompatible versions": "Incompatible versions",
|
||||||
|
"Incompatible versions!": "Incompatible versions!",
|
||||||
|
"Inspector": "Inspector",
|
||||||
|
"Invite": "Invite",
|
||||||
|
"Invite people": "Invite people",
|
||||||
|
"Join call": "Join call",
|
||||||
|
"Join call now": "Join call now",
|
||||||
|
"Join existing call?": "Join existing call?",
|
||||||
|
"Leave": "Leave",
|
||||||
|
"Loading room…": "Loading room…",
|
||||||
|
"Loading…": "Loading…",
|
||||||
|
"Local volume": "Local volume",
|
||||||
|
"Logging in…": "Logging in…",
|
||||||
|
"Login": "Login",
|
||||||
|
"Login to your account": "Login to your account",
|
||||||
|
"Microphone": "Microphone",
|
||||||
|
"Microphone {{n}}": "Microphone {{n}}",
|
||||||
|
"Microphone permissions needed to join the call.": "Microphone permissions needed to join the call.",
|
||||||
|
"More": "More",
|
||||||
|
"More menu": "More menu",
|
||||||
|
"Mute microphone": "Mute microphone",
|
||||||
|
"No": "No",
|
||||||
|
"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>",
|
||||||
|
"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",
|
||||||
|
"Passwords must match": "Passwords must match",
|
||||||
|
"Press and hold spacebar to talk": "Press and hold spacebar to talk",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Press and hold spacebar to talk over {{name}}",
|
||||||
|
"Press and hold to talk": "Press and hold to talk",
|
||||||
|
"Press and hold to talk over {{name}}": "Press and hold to talk over {{name}}",
|
||||||
|
"Profile": "Profile",
|
||||||
|
"Recaptcha dismissed": "Recaptcha dismissed",
|
||||||
|
"Recaptcha not loaded": "Recaptcha not loaded",
|
||||||
|
"Register": "Register",
|
||||||
|
"Registering…": "Registering…",
|
||||||
|
"Release spacebar key to stop": "Release spacebar key to stop",
|
||||||
|
"Release to stop": "Release to stop",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"Return to home screen": "Return to home screen",
|
||||||
|
"Save": "Save",
|
||||||
|
"Saving…": "Saving…",
|
||||||
|
"Select an option": "Select an option",
|
||||||
|
"Send debug logs": "Send debug logs",
|
||||||
|
"Sending debug logs…": "Sending debug logs…",
|
||||||
|
"Sending…": "Sending…",
|
||||||
|
"Settings": "Settings",
|
||||||
|
"Share screen": "Share screen",
|
||||||
|
"Show call inspector": "Show call inspector",
|
||||||
|
"Sign in": "Sign in",
|
||||||
|
"Sign out": "Sign out",
|
||||||
|
"Spatial audio": "Spatial audio",
|
||||||
|
"Speaker": "Speaker",
|
||||||
|
"Speaker {{n}}": "Speaker {{n}}",
|
||||||
|
"Spotlight": "Spotlight",
|
||||||
|
"Stop sharing screen": "Stop sharing screen",
|
||||||
|
"Submit feedback": "Submit feedback",
|
||||||
|
"Submitting feedback…": "Submitting feedback…",
|
||||||
|
"Take me Home": "Take me Home",
|
||||||
|
"Talk over speaker": "Talk over speaker",
|
||||||
|
"Talking…": "Talking…",
|
||||||
|
"Thanks! We'll get right on it.": "Thanks! We'll get right on it.",
|
||||||
|
"This call already exists, would you like to join?": "This call already exists, would you like to join?",
|
||||||
|
"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>Terms and conditions</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>Terms and conditions</12>",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)",
|
||||||
|
"Turn off camera": "Turn off camera",
|
||||||
|
"Turn on camera": "Turn on camera",
|
||||||
|
"Unmute microphone": "Unmute microphone",
|
||||||
|
"User ID": "User ID",
|
||||||
|
"User menu": "User menu",
|
||||||
|
"Username": "Username",
|
||||||
|
"Version: {{version}}": "Version: {{version}}",
|
||||||
|
"Video": "Video",
|
||||||
|
"Video call": "Video call",
|
||||||
|
"Video call name": "Video call name",
|
||||||
|
"Waiting for network": "Waiting for network",
|
||||||
|
"Waiting for other participants…": "Waiting for other participants…",
|
||||||
|
"Walkie-talkie call": "Walkie-talkie call",
|
||||||
|
"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",
|
||||||
|
"You can't talk at the same time": "You can't talk at the same time",
|
||||||
|
"Your recent calls": "Your recent calls"
|
||||||
|
}
|
||||||
133
public/locales/fr/app.json
Normal file
133
public/locales/fr/app.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Créer un compte</0> Or <2>Accès invité</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Mince, une erreur est survenue.</0><1>Envoyer les journaux de débogage nous aidera à résoudre le problème.</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>Pourquoi ne pas créer un mot de passe pour conserver votre compte ?</0><1>Vous pourrez garder votre nom et définir un avatar pour vos futurs appels</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Autorisez l’accès à votre caméra et microphone pour rejoindre l’appel.",
|
||||||
|
"Accept microphone permissions to join the call.": "Autorisez l’accès au microphone pour rejoindre l’appel.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Un autre utilisateur dans cet appel a un problème. Pour nous permettre de résoudre le problème, nous aimerions récupérer un journal de débogage.",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Commencer » vous acceptez nos <2>conditions d’utilisation</2>",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Rejoindre l’appel » vous acceptez nos <2>conditions d’utilisation</2>",
|
||||||
|
"Call link copied": "Lien de l’appel copié",
|
||||||
|
"Call type menu": "Menu de type d’appel",
|
||||||
|
"Camera": "Caméra",
|
||||||
|
"Camera {{n}}": "Caméra {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Accès à la caméra et au microphone requis pour rejoindre l’appel.",
|
||||||
|
"Change layout": "Changer la disposition",
|
||||||
|
"Close": "Fermer",
|
||||||
|
"Confirm password": "Confirmer le mot de passe",
|
||||||
|
"Connection lost": "Connexion interrompue",
|
||||||
|
"Copied!": "Copié !",
|
||||||
|
"Copy and share this call link": "Copier et partager le lien de cet appel",
|
||||||
|
"Copy call link and join later": "Copier le lien de cet appel et rejoindre plus tard",
|
||||||
|
"Create account": "Créer un compte",
|
||||||
|
"Debug log": "Journal de débogage",
|
||||||
|
"Debug log request": "Demande d’un journal de débogage",
|
||||||
|
"Description (optional)": "Description (facultatif)",
|
||||||
|
"Details": "Informations",
|
||||||
|
"Developer": "Développeur",
|
||||||
|
"Display name": "Nom d’affichage",
|
||||||
|
"Download debug logs": "Télécharger les journaux de débogage",
|
||||||
|
"Entering room…": "Entrée dans le salon…",
|
||||||
|
"Exit full screen": "Quitter le plein écran",
|
||||||
|
"Freedom": "Libre",
|
||||||
|
"Full screen": "Plein écran",
|
||||||
|
"Go": "Commencer",
|
||||||
|
"Grid layout menu": "Menu en grille",
|
||||||
|
"Having trouble? Help us fix it.": "Un problème ? Aidez nous à le résoudre.",
|
||||||
|
"Home": "Accueil",
|
||||||
|
"Include debug logs": "Inclure les journaux de débogage",
|
||||||
|
"Incompatible versions": "Versions incompatibles",
|
||||||
|
"Incompatible versions!": "Versions incompatibles !",
|
||||||
|
"Inspector": "Inspecteur",
|
||||||
|
"Invite people": "Inviter des gens",
|
||||||
|
"Join call": "Rejoindre l’appel",
|
||||||
|
"Join call now": "Rejoindre l’appel maintenant",
|
||||||
|
"Join existing call?": "Rejoindre un appel existant ?",
|
||||||
|
"Leave": "Partir",
|
||||||
|
"Loading room…": "Chargement du salon…",
|
||||||
|
"Loading…": "Chargement…",
|
||||||
|
"Local volume": "Volume local",
|
||||||
|
"Logging in…": "Connexion…",
|
||||||
|
"Login": "Connexion",
|
||||||
|
"Login to your account": "Connectez vous à votre compte",
|
||||||
|
"Microphone": "Microphone",
|
||||||
|
"Microphone permissions needed to join the call.": "Accès au microphone requis pour rejoindre l’appel.",
|
||||||
|
"Microphone {{n}}": "Microphone {{n}}",
|
||||||
|
"More": "Plus",
|
||||||
|
"More menu": "Menu plus",
|
||||||
|
"Mute microphone": "Couper le micro",
|
||||||
|
"No": "Non",
|
||||||
|
"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>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Des utilisateurs essayent de rejoindre cet appel à partir de versions incompatibles. Ces utilisateurs doivent rafraîchir la page dans leur navigateur : <1>{userLis}</1>",
|
||||||
|
"Password": "Mot de passe",
|
||||||
|
"Passwords must match": "Les mots de passe doivent correspondre",
|
||||||
|
"Press and hold spacebar to talk": "Appuyez et maintenez la barre d’espace enfoncée pour parler",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Appuyez et maintenez la barre d’espace enfoncée pour parler par dessus {{name}}",
|
||||||
|
"Press and hold to talk": "Appuyez et maintenez enfoncé pour parler",
|
||||||
|
"Press and hold to talk over {{name}}": "Appuyez et maintenez enfoncé pour parler par dessus {{name}}",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Recaptcha dismissed": "Recaptcha refusé",
|
||||||
|
"Recaptcha not loaded": "Recaptcha non chargé",
|
||||||
|
"Register": "S’enregistrer",
|
||||||
|
"Registering…": "Enregistrement…",
|
||||||
|
"Release spacebar key to stop": "Relâcher la barre d’espace pour arrêter",
|
||||||
|
"Release to stop": "Relâcher pour arrêter",
|
||||||
|
"Remove": "Supprimer",
|
||||||
|
"Return to home screen": "Retour à l’accueil",
|
||||||
|
"Save": "Enregistrer",
|
||||||
|
"Saving…": "Enregistrement…",
|
||||||
|
"Select an option": "Sélectionnez une option",
|
||||||
|
"Send debug logs": "Envoyer les journaux de débogage",
|
||||||
|
"Sending…": "Envoi…",
|
||||||
|
"Settings": "Paramètres",
|
||||||
|
"Share screen": "Partage d’écran",
|
||||||
|
"Show call inspector": "Afficher l’inspecteur d’appel",
|
||||||
|
"Sign in": "Connexion",
|
||||||
|
"Sign out": "Déconnexion",
|
||||||
|
"Spatial audio": "Audio spatialisé",
|
||||||
|
"Spotlight": "Premier plan",
|
||||||
|
"Stop sharing screen": "Arrêter le partage d’écran",
|
||||||
|
"Submit feedback": "Envoyer des retours",
|
||||||
|
"Submitting feedback…": "Envoi des retours…",
|
||||||
|
"Take me Home": "Retouner à l’accueil",
|
||||||
|
"Talk over speaker": "Parler par dessus l’intervenant",
|
||||||
|
"Thanks! We'll get right on it.": "Merci ! Nous allons nous y attaquer.",
|
||||||
|
"This call already exists, would you like to join?": "Cet appel existe déjà, voulez-vous le rejoindre ?",
|
||||||
|
"{{name}} is presenting": "{{name}} est le présentateur",
|
||||||
|
"Fetching group call timed out.": "Échec de connexion à l’appel de groupe dans le temps imparti.",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} — Appel talkie-walkie",
|
||||||
|
"{{name}} is talking…": "{{name}} est en train de parler…",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, votre appel est désormais terminé",
|
||||||
|
"{{count}} people connected|other": "{{count}} personnes connectées",
|
||||||
|
"{{count}} people connected|one": "{{count}} personne connectée",
|
||||||
|
"Your recent calls": "Appels récents",
|
||||||
|
"You can't talk at the same time": "Vous ne pouvez pas parler en même temps",
|
||||||
|
"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.",
|
||||||
|
"Walkie-talkie call name": "Nom de l’appel talkie-walkie",
|
||||||
|
"Walkie-talkie call": "Appel talkie-walkie",
|
||||||
|
"Waiting for other participants…": "En attente d’autres participants…",
|
||||||
|
"Waiting for network": "En attente du réseau",
|
||||||
|
"Video call name": "Nom de l’appel vidéo",
|
||||||
|
"Video call": "Appel vidéo",
|
||||||
|
"Video": "Vidéo",
|
||||||
|
"Version: {{version}}": "Version : {{version}}",
|
||||||
|
"Username": "Nom d’utilisateur",
|
||||||
|
"User menu": "Menu utilisateur",
|
||||||
|
"User ID": "Identifiant utilisateur",
|
||||||
|
"Unmute microphone": "Allumer le micro",
|
||||||
|
"Turn on camera": "Allumer la caméra",
|
||||||
|
"Turn off camera": "Couper la caméra",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Cela donnera l’impression que le son de l’intervenant provient de là où leur tuile est positionnée sur l’écran. (Fonctionnalité expérimentale : ceci pourrait avoir un impact sur la stabilité du son.)",
|
||||||
|
"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>Terms and conditions</12>": "Ce site est protégé par ReCAPTCHA, la <2>politique de confidentialité</2> et les <6>conditions d’utilisation</6> de Google s’appliquent.<9></9>En cliquant sur « S’enregistrer » vous acceptez également nos <12>conditions d’utilisation</12>",
|
||||||
|
"Talking…": "Vous parlez…",
|
||||||
|
"Speaker {{n}}": "Intervenant {{n}}",
|
||||||
|
"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>",
|
||||||
|
"Sending debug logs…": "Envoi des journaux de débogage…"
|
||||||
|
}
|
||||||
132
public/locales/id/app.json
Normal file
132
public/locales/id/app.json
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Sudah punya akun?</0><1><0>Masuk</0> Atau <2>Akses sebagai tamu</2></1>",
|
||||||
|
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Buat akun</0> Atau <2>Akses sebagai tamu</2>",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Aduh, ada yang salah.</0><1>Mengirimkan catatan pengawakutuan akan membantu kami melacak masalahnya.</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>Kenapa tidak selesaikan dengan mengatur sebuah kata sandi untuk menjaga akun Anda?</0><1>Anda akan dapat tetap menggunakan nama Anda dan atur sebuah avatar untuk digunakan dalam panggilan di masa mendatang</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Terima izin kamera/mikrofon untuk bergabung ke panggilan.",
|
||||||
|
"Accept microphone permissions to join the call.": "Terima izin mikrofon untuk bergabung ke panggilan.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Pengguna yang lain di panggilan ini sedang mengalami masalah. Supaya dapat mendiagnosa masalah ini, kami ingin mengumpulkan sebuah catatan pengawakutuan.",
|
||||||
|
"Audio": "Audio",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung\", Anda terima <2>syarat dan ketentuan</2> kami",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda terima <2>syarat dan ketentuan</2> kami",
|
||||||
|
"Call link copied": "Tautan panggilan disalin",
|
||||||
|
"Call type menu": "Menu jenis panggilan",
|
||||||
|
"Camera": "Kamera",
|
||||||
|
"Camera {{n}}": "Kamera {{n}}",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Izin kamera/mikrofon dibutuhkan untuk bergabung ke panggilan.",
|
||||||
|
"Change layout": "Ubah tata letak",
|
||||||
|
"Close": "Tutup",
|
||||||
|
"Confirm password": "Konfirmasi kata sandi",
|
||||||
|
"Connection lost": "Koneksi hilang",
|
||||||
|
"Copied!": "Disalin!",
|
||||||
|
"Copy and share this call link": "Salin dan bagikan tautan panggilan ini",
|
||||||
|
"Copy call link and join later": "Salin tautan panggilan dan bergabung nanti",
|
||||||
|
"Create account": "Buat akun",
|
||||||
|
"Debug log": "Catatan pengawakutuan",
|
||||||
|
"Debug log request": "Permintaan catatan pengawakutuan",
|
||||||
|
"Description (optional)": "Deskripsi (opsional)",
|
||||||
|
"Details": "Detail",
|
||||||
|
"Developer": "Pengembang",
|
||||||
|
"Display name": "Nama tampilan",
|
||||||
|
"Download debug logs": "Unduh catatan pengawakutuan",
|
||||||
|
"Entering room…": "Memasuki ruangan…",
|
||||||
|
"Exit full screen": "Keluar dari layar penuh",
|
||||||
|
"Fetching group call timed out.": "Waktu pendapatan panggilan grup habis.",
|
||||||
|
"Freedom": "Bebas",
|
||||||
|
"Full screen": "Layar penuh",
|
||||||
|
"Go": "Bergabung",
|
||||||
|
"Grid layout menu": "Menu tata letak kisi",
|
||||||
|
"Having trouble? Help us fix it.": "Mengalami masalah? Bantu kami memperbaikinya.",
|
||||||
|
"Home": "Beranda",
|
||||||
|
"Include debug logs": "Termasuk catatan pengawakutuan",
|
||||||
|
"Incompatible versions": "Versi tidak kompatibel",
|
||||||
|
"Incompatible versions!": "Versi tidak kompatibel!",
|
||||||
|
"Inspector": "Inspektur",
|
||||||
|
"Invite": "Undang",
|
||||||
|
"Invite people": "Undang orang",
|
||||||
|
"Join call": "Bergabung ke panggilan",
|
||||||
|
"Join call now": "Bergabung ke panggilan sekarang",
|
||||||
|
"Join existing call?": "Bergabung ke panggilan yang sudah ada?",
|
||||||
|
"Leave": "Keluar",
|
||||||
|
"Loading room…": "Memuat ruangan…",
|
||||||
|
"Loading…": "Memuat…",
|
||||||
|
"Local volume": "Volume lokal",
|
||||||
|
"Logging in…": "Memasuki…",
|
||||||
|
"Login": "Masuk",
|
||||||
|
"Login to your account": "Masuk ke akun Anda",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Microphone permissions needed to join the call.": "Izin mikrofon dibutuhkan untuk bergabung ke panggilan ini.",
|
||||||
|
"Microphone {{n}}": "Mikrofon {{n}}",
|
||||||
|
"More": "Lainnya",
|
||||||
|
"More menu": "Menu lainnya",
|
||||||
|
"Mute microphone": "Bisukan mikrofon",
|
||||||
|
"No": "Tidak",
|
||||||
|
"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>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Pengguna lain sedang mencoba bergabung ke panggilan ini dari versi yang tidak kompatibel. Pengguna berikut seharusnya memastikan bahwa mereka telah memuat ulang peramban mereka: <1>{userLis}</1>",
|
||||||
|
"Password": "Kata sandi",
|
||||||
|
"Passwords must match": "Kata sandi harus cocok",
|
||||||
|
"Press and hold spacebar to talk": "Tekan dan tahan bilah spasi untuk berbicara",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Tekan dan tahan bilah spasi untuk berbicara pada {{name}}",
|
||||||
|
"Press and hold to talk": "Tekan dan tahan untuk berbicara",
|
||||||
|
"Press and hold to talk over {{name}}": "Tekan dan tahan untuk berbicara pada {{name}}",
|
||||||
|
"Profile": "Profil",
|
||||||
|
"Recaptcha dismissed": "Recaptcha ditutup",
|
||||||
|
"Recaptcha not loaded": "Recaptcha tidak dimuat",
|
||||||
|
"Register": "Daftar",
|
||||||
|
"Registering…": "Mendaftarkan…",
|
||||||
|
"Release spacebar key to stop": "Lepaskan bilah spasi untuk berhenti",
|
||||||
|
"Release to stop": "Lepaskan untuk berhenti",
|
||||||
|
"Remove": "Hapus",
|
||||||
|
"Return to home screen": "Kembali ke layar beranda",
|
||||||
|
"Save": "Simpan",
|
||||||
|
"Saving…": "Menyimpan…",
|
||||||
|
"Select an option": "Pilih sebuah opsi",
|
||||||
|
"Send debug logs": "Kirim catatan pengawakutuan",
|
||||||
|
"Sending…": "Mengirimkan…",
|
||||||
|
"Settings": "Pengaturan",
|
||||||
|
"Share screen": "Bagikan layar",
|
||||||
|
"Show call inspector": "Tampilkan inspektur panggilan",
|
||||||
|
"Sign in": "Masuk",
|
||||||
|
"Sign out": "Keluar",
|
||||||
|
"Spatial audio": "Audio spasial",
|
||||||
|
"Speaker": "Pembicara",
|
||||||
|
"Speaker {{n}}": "Pembicara {{n}}",
|
||||||
|
"Spotlight": "Sorotan",
|
||||||
|
"Stop sharing screen": "Berhenti membagikan layar",
|
||||||
|
"Submit feedback": "Kirim masukan",
|
||||||
|
"Submitting feedback…": "Mengirimkan masukan…",
|
||||||
|
"Take me Home": "Bawa saya ke Beranda",
|
||||||
|
"Talk over speaker": "Bicara pada pembicara",
|
||||||
|
"Talking…": "Berbicara…",
|
||||||
|
"Thanks! We'll get right on it.": "Terima kasih! Kami akan melihatnya.",
|
||||||
|
"This call already exists, would you like to join?": "Panggilan ini sudah ada, apakah Anda ingin bergabung?",
|
||||||
|
"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>Terms and conditions</12>": "Situs ini dilindungi oleh ReCAPTCHA dan <2>Kebijakan Privasi</2> dan <6>Ketentuan Layanan</6> Google berlaku.<9>Dengan mengeklik \"Daftar\", Anda terima <12>syarat dan ketentuan</12> kami",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Ini akan membuat suara pembicara seolah-olah berasal dari tempat ubin mereka diposisikan di layar. (Fitur uji coba: ini dapat memengaruhi stabilitas audio.)",
|
||||||
|
"Turn off camera": "Matikan kamera",
|
||||||
|
"Turn on camera": "Nyalakan kamera",
|
||||||
|
"Unmute microphone": "Suarakan mikrofon",
|
||||||
|
"User ID": "ID pengguna",
|
||||||
|
"User menu": "Menu pengguna",
|
||||||
|
"Username": "Nama pengguna",
|
||||||
|
"Version: {{version}}": "Versi: {{version}}",
|
||||||
|
"Video": "Video",
|
||||||
|
"Video call": "Panggilan video",
|
||||||
|
"Video call name": "Nama panggilan video",
|
||||||
|
"Waiting for network": "Menunggu jaringan",
|
||||||
|
"Waiting for other participants…": "Menunggu peserta lain…",
|
||||||
|
"Walkie-talkie call": "Panggilan protofon",
|
||||||
|
"Walkie-talkie call name": "Nama panggilan protofon",
|
||||||
|
"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",
|
||||||
|
"You can't talk at the same time": "Anda tidak dapat berbicara pada waktu yang sama",
|
||||||
|
"Your recent calls": "Panggilan Anda terkini",
|
||||||
|
"{{count}} people connected|one": "{{count}} orang terhubung",
|
||||||
|
"{{count}} people connected|other": "{{count}} orang terhubung",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, panggilan Anda sekarang telah berakhir",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is presenting": "{{name}} sedang mempresentasi",
|
||||||
|
"{{name}} is talking…": "{{name}} sedang berbicara…",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Panggilan protofon"
|
||||||
|
}
|
||||||
12
public/locales/ko/app.json
Normal file
12
public/locales/ko/app.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"<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>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "",
|
||||||
|
"{{count}} people connected|one": "{{count}}명 연결됨",
|
||||||
|
"{{count}} people connected|other": "{{count}}명 연결됨",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}님, 전화가 종료되었습니다",
|
||||||
|
"{{names}}, {{name}}": "{{names}}님, {{name}}님",
|
||||||
|
"{{name}} is presenting": "{{name}}님이 발표 중",
|
||||||
|
"{{name}} is talking…": "{{name}}님이 말하는 중…",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - 워키토키 전화"
|
||||||
|
}
|
||||||
133
public/locales/ru/app.json
Normal file
133
public/locales/ru/app.json
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"Register": "Зарегистрироваться",
|
||||||
|
"Saving…": "Сохранение…",
|
||||||
|
"Registering…": "Регистрация…",
|
||||||
|
"Logging in…": "Вход…",
|
||||||
|
"Entering room…": "Вход в комнату…",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"Waiting for other participants…": "Ожидание других участников…",
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)": "Эта функция балансирует звук к расположению плитки на экране. (Экспериментальная функция: может повлиять на стабильность аудио.)",
|
||||||
|
"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>Terms and conditions</12>": "Этот сайт защищён ReCAPTCHA от Google, ознакомьтесь с их <2>Политикой конфиденциальности</2> и <6>Пользовательским соглашением</6>.<9></9>Нажимая \"Зарегистрироваться\", вы также принимаете наши <12>Положения и условия</12>.",
|
||||||
|
"This call already exists, would you like to join?": "Этот звонок уже существует, хотите присоединиться?",
|
||||||
|
"Thanks! We'll get right on it.": "Спасибо! Мы учтём ваш отзыв.",
|
||||||
|
"Talking…": "Говорите…",
|
||||||
|
"Submitting feedback…": "Отправка отзыва…",
|
||||||
|
"Submit feedback": "Отправить отзыв",
|
||||||
|
"Sending debug logs…": "Отправка журнала отладки…",
|
||||||
|
"Select an option": "Выберите вариант",
|
||||||
|
"Release to stop": "Отпустите, чтобы прекратить вещание",
|
||||||
|
"Release spacebar key to stop": "Чтобы прекратить вещание, отпустите [Пробел]",
|
||||||
|
"Press and hold to talk over {{name}}": "Зажмите, чтобы говорить поверх участника {{name}}",
|
||||||
|
"Press and hold spacebar to talk over {{name}}": "Чтобы говорить поверх участника {{name}}, нажмите и удерживайте [Пробел]",
|
||||||
|
"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": "Меню \"Расположение сеткой\"",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Присоединиться сейчас\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "Нажимая \"Далее\", вы соглашаетесь с нашими <2>положениями и условиями</2>",
|
||||||
|
"<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>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>",
|
||||||
|
"Your recent calls": "Ваши недавние звонки",
|
||||||
|
"You can't talk at the same time": "Вы не можете говорить одновременно",
|
||||||
|
"Yes, join call": "Да, присоединиться",
|
||||||
|
"WebRTC is not supported or is being blocked in this browser.": "WebRTC не поддерживается или заблокирован в этом браузере.",
|
||||||
|
"Walkie-talkie call name": "Название звонка-рации",
|
||||||
|
"Walkie-talkie call": "Звонок-рация",
|
||||||
|
"Waiting for network": "Ожидание сети",
|
||||||
|
"Video call name": "Название видео-звонка",
|
||||||
|
"Video call": "Видео-звонок",
|
||||||
|
"Video": "Видео",
|
||||||
|
"Version: {{version}}": "Версия: {{version}}",
|
||||||
|
"Username": "Имя пользователя",
|
||||||
|
"User menu": "Меню пользователя",
|
||||||
|
"User ID": "ID пользователя",
|
||||||
|
"Unmute microphone": "Включить микрофон",
|
||||||
|
"Turn on camera": "Включить камеру",
|
||||||
|
"Turn off camera": "Отключить камеру",
|
||||||
|
"Talk over speaker": "Говорить через динамик",
|
||||||
|
"Take me Home": "Перейти в Начало",
|
||||||
|
"Stop sharing screen": "Остановить показ экрана",
|
||||||
|
"Spotlight": "Внимание",
|
||||||
|
"Speaker {{n}}": "Динамик {{n}}",
|
||||||
|
"Speaker": "Динамик",
|
||||||
|
"Spatial audio": "Пространственное аудио",
|
||||||
|
"Sign out": "Выйти",
|
||||||
|
"Sign in": "Войти",
|
||||||
|
"Show call inspector": "Показать инспектор",
|
||||||
|
"Share screen": "Поделиться экраном",
|
||||||
|
"Settings": "Настройки",
|
||||||
|
"Sending…": "Отправка…",
|
||||||
|
"Local volume": "Местная громкость",
|
||||||
|
"Call type menu": "Меню \"Тип звонка\"",
|
||||||
|
"More menu": "Полное меню",
|
||||||
|
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Звонок-рация",
|
||||||
|
"Include debug logs": "Приложить журнал отладки",
|
||||||
|
"Download debug logs": "Скачать журнал отладки",
|
||||||
|
"Debug log request": "Запрос журнала отладки",
|
||||||
|
"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.": "У одного из участников звонка есть неполадки. Чтобы лучше диагностировать похожие проблемы, нам нужен журнал отладки.",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Ой, что-то пошло не так.</0><1>Отправив журнал отладки, вы поможете нам найти проблемный участок.</1>",
|
||||||
|
"Send debug logs": "Отправить журнал отладки",
|
||||||
|
"Save": "Сохранить",
|
||||||
|
"Return to home screen": "Вернуться в начало",
|
||||||
|
"Remove": "Удалить",
|
||||||
|
"Recaptcha not loaded": "Невозможно начать проверку",
|
||||||
|
"Recaptcha dismissed": "Проверка не пройдена",
|
||||||
|
"Profile": "Профиль",
|
||||||
|
"Press and hold to talk": "Зажмите, чтобы говорить",
|
||||||
|
"Press and hold spacebar to talk": "Чтобы говорить, нажмите и удерживайте [Пробел]",
|
||||||
|
"Passwords must match": "Пароли должны совпадать",
|
||||||
|
"Password": "Пароль",
|
||||||
|
"Not registered yet? <2>Create an account</2>": "Ещё не зарегистрированы? <2>Создайте аккаунт</2>",
|
||||||
|
"Not now, return to home screen": "Не сейчас, вернитесь в начало",
|
||||||
|
"No": "Нет",
|
||||||
|
"Mute microphone": "Отключить микрофон",
|
||||||
|
"More": "Больше",
|
||||||
|
"Microphone permissions needed to join the call.": "Нужно разрешение на доступ к микрофону для присоединения к звонку.",
|
||||||
|
"Microphone {{n}}": "Микрофон {{n}}",
|
||||||
|
"Microphone": "Микрофон",
|
||||||
|
"Login to your account": "Войдите в свой аккаунт",
|
||||||
|
"Login": "Вход",
|
||||||
|
"Loading…": "Загрузка…",
|
||||||
|
"Loading room…": "Загрузка комнаты…",
|
||||||
|
"Leave": "Покинуть",
|
||||||
|
"Join existing call?": "Присоединиться к существующему звонку?",
|
||||||
|
"Join call now": "Присоединиться сейчас",
|
||||||
|
"Join call": "Присоединиться",
|
||||||
|
"Invite people": "Пригласить участников",
|
||||||
|
"Invite": "Пригласить",
|
||||||
|
"Inspector": "Инспектор",
|
||||||
|
"Incompatible versions!": "Несовместимые версии!",
|
||||||
|
"Incompatible versions": "Несовместимые версии",
|
||||||
|
"Home": "Начало",
|
||||||
|
"Having trouble? Help us fix it.": "Есть проблема? Помогите нам её устранить.",
|
||||||
|
"Go": "Далее",
|
||||||
|
"Full screen": "Полноэкранный режим",
|
||||||
|
"Freedom": "Свобода",
|
||||||
|
"Fetching group call timed out.": "Истекло время ожидания для группового звонка.",
|
||||||
|
"Exit full screen": "Выйти из полноэкранного режима",
|
||||||
|
"Display name": "Видимое имя",
|
||||||
|
"Developer": "Разработчик",
|
||||||
|
"Details": "Подробности",
|
||||||
|
"Description (optional)": "Описание (необязательно)",
|
||||||
|
"Create account": "Создать аккаунт",
|
||||||
|
"Copy call link and join later": "Скопировать ссылку и присоединиться позже",
|
||||||
|
"Copy and share this call link": "Скопируйте и поделитесь этой ссылкой на звонок",
|
||||||
|
"Copied!": "Скопировано!",
|
||||||
|
"Connection lost": "Соединение потеряно",
|
||||||
|
"Confirm password": "Подтвердите пароль",
|
||||||
|
"Close": "Закрыть",
|
||||||
|
"Change layout": "Изменить расположение",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Нужны разрешения на доступ к камере/микрофону для присоединения к звонку.",
|
||||||
|
"Camera {{n}}": "Камера {{n}}",
|
||||||
|
"Camera": "Камера",
|
||||||
|
"Call link copied": "Ссылка на звонок скопирована",
|
||||||
|
"Avatar": "Аватар",
|
||||||
|
"Audio": "Аудио",
|
||||||
|
"Accept microphone permissions to join the call.": "Для присоединения к звонку разрешите доступ к микрофону.",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Для присоединения к звонку разрешите доступ к камере/микрофону.",
|
||||||
|
"{{name}} is talking…": "{{name}} говорит…",
|
||||||
|
"{{name}} is presenting": "{{name}} показывает",
|
||||||
|
"{{displayName}}, your call is now ended": "{{displayName}}, ваш звонок завершён",
|
||||||
|
"{{count}} people connected|other": "{{count}} подключилось",
|
||||||
|
"{{count}} people connected|one": "{{count}} подключился"
|
||||||
|
}
|
||||||
103
public/locales/tr/app.json
Normal file
103
public/locales/tr/app.json
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"<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>Hesabınızı tutmak için niye bir parola açmıyorsunuz?</0><1>Böylece ileriki aramalarda adınızı ve avatarınızı kullanabileceksiniz</1>",
|
||||||
|
"Accept camera/microphone permissions to join the call.": "Aramaya katılmanız için kamera/mikrofon erişimine izin verin.",
|
||||||
|
"Accept microphone permissions to join the call.": "Aramaya katılmak için mikrofon erişim izni verin.",
|
||||||
|
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.": "Bu aramadaki başka bir kullanıcı sorun yaşıyor. Sorunu daha iyi çözebilmemiz için hata ayıklama kütüğünü almak isteriz.",
|
||||||
|
"Audio": "Ses",
|
||||||
|
"Avatar": "Avatar",
|
||||||
|
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "\"Git\"e tıklayarak,<2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
|
||||||
|
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "\"Şimdi katıl\"a tıklayarak, <2>hükümler ve koşullar</2>ı kabul etmiş sayılırsınız",
|
||||||
|
"Call link copied": "Arama bağlantısı kopyalandı",
|
||||||
|
"Call type menu": "Arama tipi menüsü",
|
||||||
|
"Camera": "Kamera",
|
||||||
|
"Camera {{n}}": "{{n}}. kamera",
|
||||||
|
"Camera/microphone permissions needed to join the call.": "Aramaya katılmak için kamera/mikrofon izinleri gerek.",
|
||||||
|
"Change layout": "Yerleşimi değiştir",
|
||||||
|
"Close": "Kapat",
|
||||||
|
"Confirm password": "Parolayı tekrar edin",
|
||||||
|
"Connection lost": "Bağlantı koptu",
|
||||||
|
"Copied!": "Kopyalandı",
|
||||||
|
"Copy and share this call link": "Arama bağlantısını kopyala ve paylaş",
|
||||||
|
"Copy call link and join later": "Sonra katılmak üzere bağlantıyı kopyala",
|
||||||
|
"Create account": "Hesap aç",
|
||||||
|
"Debug log": "Hata ayıklama kütüğü",
|
||||||
|
"Debug log request": "Hata ayıklama kütük istemi",
|
||||||
|
"Description (optional)": "Tanım (isteğe bağlı)",
|
||||||
|
"Details": "Ayrıntı",
|
||||||
|
"Developer": "Geliştirici",
|
||||||
|
"Display name": "Ekran adı",
|
||||||
|
"Download debug logs": "Hata ayıklama kütüğünü indir",
|
||||||
|
"Entering room…": "Odaya giriliyor…",
|
||||||
|
"Exit full screen": "Tam ekranı terk et",
|
||||||
|
"Fetching group call timed out.": "Grup çağrısı zaman aşımına uğradı.",
|
||||||
|
"Freedom": "Özgürlük",
|
||||||
|
"Full screen": "Tam ekran",
|
||||||
|
"Go": "Git",
|
||||||
|
"Grid layout menu": "Izgara plan menü",
|
||||||
|
"Having trouble? Help us fix it.": "Sorun mu var? Çözmemize yardım edin.",
|
||||||
|
"Home": "Ev",
|
||||||
|
"Include debug logs": "Hata ayıklama kütüğünü dahil et",
|
||||||
|
"Incompatible versions": "Uyumsuz sürümler",
|
||||||
|
"Incompatible versions!": "Sürüm uyumsuz!",
|
||||||
|
"Inspector": "Denetçi",
|
||||||
|
"Invite people": "Kişileri davet et",
|
||||||
|
"Join call": "Aramaya katıl",
|
||||||
|
"Join call now": "Aramaya katıl",
|
||||||
|
"Join existing call?": "Mevcut aramaya katıl?",
|
||||||
|
"Leave": "Çık",
|
||||||
|
"Loading room…": "Oda yükleniyor…",
|
||||||
|
"Loading…": "Yükleniyor…",
|
||||||
|
"Local volume": "Yerel ses seviyesi",
|
||||||
|
"Logging in…": "Giriliyor…",
|
||||||
|
"Login": "Gir",
|
||||||
|
"Login to your account": "Hesabınıza girin",
|
||||||
|
"Microphone": "Mikrofon",
|
||||||
|
"Microphone permissions needed to join the call.": "Aramaya katılmak için mikrofon erişim izni gerek.",
|
||||||
|
"Microphone {{n}}": "{{n}}. mikrofon",
|
||||||
|
"More": "Daha",
|
||||||
|
"More menu": "Daha fazla",
|
||||||
|
"Mute microphone": "Mikrofonu kapat",
|
||||||
|
"No": "Hayır",
|
||||||
|
"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>",
|
||||||
|
"Other users are trying to join this call from incompatible versions. These users should ensure that they have refreshed their browsers:<1>{userLis}</1>": "Başka kullanıcılar uyumsuz sürümden katılmaya çalışıyorlar. <1>{userLis}</1> tarayıcılarını mutlaka tazelemeliler.",
|
||||||
|
"Password": "Parola",
|
||||||
|
"Passwords must match": "Parolalar aynı olmalı",
|
||||||
|
"Press and hold spacebar to talk": "Konuşmak için boşluk çubuğunu basılı tutun",
|
||||||
|
"Press and hold to talk": "Konuşmak için basılı tutun",
|
||||||
|
"Recaptcha dismissed": "reCAPTCHA atlandı",
|
||||||
|
"Recaptcha not loaded": "reCAPTCHA yüklenmedi",
|
||||||
|
"Register": "Kaydol",
|
||||||
|
"Registering…": "Kaydediyor…",
|
||||||
|
"Release spacebar key to stop": "Kesmek için boşluk tuşunu bırakın",
|
||||||
|
"Release to stop": "Kesmek için bırakın",
|
||||||
|
"Remove": "Çıkar",
|
||||||
|
"Return to home screen": "Ev ekranına geri dön",
|
||||||
|
"Save": "Kaydet",
|
||||||
|
"Saving…": "Kaydediliyor…",
|
||||||
|
"Select an option": "Bir seçenek seç",
|
||||||
|
"Send debug logs": "Hata ayıklama kütüğünü gönder",
|
||||||
|
"Sending…": "Gönderiliyor…",
|
||||||
|
"Settings": "Ayarlar",
|
||||||
|
"Share screen": "Ekran paylaş",
|
||||||
|
"Show call inspector": "Arama denetçisini göster",
|
||||||
|
"Sign in": "Gir",
|
||||||
|
"Sign out": "Çık",
|
||||||
|
"Spatial audio": "Uzamsal ses",
|
||||||
|
"Stop sharing screen": "Ekran paylaşmayı terk et",
|
||||||
|
"Submit feedback": "Geri bildirim ver",
|
||||||
|
"Submitting feedback…": "Geri bildirimler gönderiliyor…",
|
||||||
|
"Take me Home": "Ev ekranına gir",
|
||||||
|
"Talking…": "Konuşuyor…",
|
||||||
|
"Thanks! We'll get right on it.": "Sağol! Bununla ilgileneceğiz.",
|
||||||
|
"This call already exists, would you like to join?": "Bu arama zaten var, katılmak ister misiniz?",
|
||||||
|
"{{count}} people connected|one": "{{count}} kişi bağlı",
|
||||||
|
"{{count}} people connected|other": "{{count}} kişi bağlı",
|
||||||
|
"{{displayName}}, your call is now ended": "Aramanız bitti, {{displayName]}!",
|
||||||
|
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||||
|
"{{name}} is presenting": "{{name}} sunuyor",
|
||||||
|
"{{name}} is talking…": "{{name}} konuşuyor…",
|
||||||
|
"<0>Oops, something's gone wrong.</0><1>Submitting debug logs will help us track down the problem.</1>": "<0>Hoop, bir şeyler yanlış.</0><1>Hata ayıklama kütüğünü göndermek sorunu incelememize yardımcı olur.</1>",
|
||||||
|
"<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>"
|
||||||
|
}
|
||||||
1
public/locales/uk/app.json
Normal file
1
public/locales/uk/app.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
60
src/App.tsx
60
src/App.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { Suspense } from "react";
|
||||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { OverlayProvider } from "@react-aria/overlays";
|
import { OverlayProvider } from "@react-aria/overlays";
|
||||||
@@ -43,34 +43,36 @@ export default function App({ history }: AppProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<ClientProvider>
|
<Suspense fallback={null}>
|
||||||
<InspectorContextProvider>
|
<ClientProvider>
|
||||||
<Sentry.ErrorBoundary fallback={errorPage}>
|
<InspectorContextProvider>
|
||||||
<OverlayProvider>
|
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||||
<Switch>
|
<OverlayProvider>
|
||||||
<SentryRoute exact path="/">
|
<Switch>
|
||||||
<HomePage />
|
<SentryRoute exact path="/">
|
||||||
</SentryRoute>
|
<HomePage />
|
||||||
<SentryRoute exact path="/login">
|
</SentryRoute>
|
||||||
<LoginPage />
|
<SentryRoute exact path="/login">
|
||||||
</SentryRoute>
|
<LoginPage />
|
||||||
<SentryRoute exact path="/register">
|
</SentryRoute>
|
||||||
<RegisterPage />
|
<SentryRoute exact path="/register">
|
||||||
</SentryRoute>
|
<RegisterPage />
|
||||||
<SentryRoute path="/room/:roomId?">
|
</SentryRoute>
|
||||||
<RoomPage />
|
<SentryRoute path="/room/:roomId?">
|
||||||
</SentryRoute>
|
<RoomPage />
|
||||||
<SentryRoute path="/inspector">
|
</SentryRoute>
|
||||||
<SequenceDiagramViewerPage />
|
<SentryRoute path="/inspector">
|
||||||
</SentryRoute>
|
<SequenceDiagramViewerPage />
|
||||||
<SentryRoute path="*">
|
</SentryRoute>
|
||||||
<RoomRedirect />
|
<SentryRoute path="*">
|
||||||
</SentryRoute>
|
<RoomRedirect />
|
||||||
</Switch>
|
</SentryRoute>
|
||||||
</OverlayProvider>
|
</Switch>
|
||||||
</Sentry.ErrorBoundary>
|
</OverlayProvider>
|
||||||
</InspectorContextProvider>
|
</Sentry.ErrorBoundary>
|
||||||
</ClientProvider>
|
</InspectorContextProvider>
|
||||||
|
</ClientProvider>
|
||||||
|
</Suspense>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { useHistory } from "react-router-dom";
|
|||||||
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ErrorView } from "./FullScreenView";
|
import { ErrorView } from "./FullScreenView";
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +36,7 @@ import {
|
|||||||
CryptoStoreIntegrityError,
|
CryptoStoreIntegrityError,
|
||||||
} from "./matrix-utils";
|
} from "./matrix-utils";
|
||||||
import { widget } from "./widget";
|
import { widget } from "./widget";
|
||||||
|
import { translatedError } from "./TranslatedError";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -146,7 +148,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
},
|
},
|
||||||
false // Don't need the crypto store just to log out
|
false // Don't need the crypto store just to log out
|
||||||
);
|
);
|
||||||
await client.logout(undefined, true);
|
await client.logout(true);
|
||||||
} catch (err_) {
|
} catch (err_) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"The previous session was lost, and we couldn't log it out, " +
|
"The previous session was lost, and we couldn't log it out, " +
|
||||||
@@ -253,7 +255,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const logout = useCallback(async () => {
|
const logout = useCallback(async () => {
|
||||||
await client.logout(undefined, true);
|
await client.logout(true);
|
||||||
await client.clearStores();
|
await client.clearStores();
|
||||||
clearSession();
|
clearSession();
|
||||||
setState({
|
setState({
|
||||||
@@ -267,6 +269,8 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
history.push("/");
|
history.push("/");
|
||||||
}, [history, client]);
|
}, [history, client]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// To protect against multiple sessions writing to the same storage
|
// To protect against multiple sessions writing to the same storage
|
||||||
// simultaneously, we send a to-device message that shuts down all other
|
// simultaneously, we send a to-device message that shuts down all other
|
||||||
@@ -287,8 +291,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
|
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
error: new Error(
|
error: translatedError(
|
||||||
"This application has been opened in another tab."
|
"This application has been opened in another tab.",
|
||||||
|
t
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -306,7 +311,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [client]);
|
}, [client, t]);
|
||||||
|
|
||||||
const context = useMemo<ClientState>(
|
const context = useMemo<ClientState>(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { HTMLAttributes } from "react";
|
import React, { HTMLAttributes, useMemo } from "react";
|
||||||
import classNames from "classnames";
|
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 styles from "./Facepile.module.css";
|
import styles from "./Facepile.module.css";
|
||||||
import { Avatar, Size, sizes } from "./Avatar";
|
import { Avatar, Size, sizes } from "./Avatar";
|
||||||
@@ -44,13 +45,25 @@ export function Facepile({
|
|||||||
size = Size.XS,
|
size = Size.XS,
|
||||||
...rest
|
...rest
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const _size = sizes.get(size);
|
const _size = sizes.get(size);
|
||||||
const _overlap = overlapMap[size];
|
const _overlap = overlapMap[size];
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
return participants.reduce<string | null>(
|
||||||
|
(prev, curr) =>
|
||||||
|
prev === null
|
||||||
|
? curr.name
|
||||||
|
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
|
||||||
|
null
|
||||||
|
) as string;
|
||||||
|
}, [participants, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.facepile, styles[size], className)}
|
className={classNames(styles.facepile, styles[size], className)}
|
||||||
title={participants.map((member) => member.name).join(", ")}
|
title={title}
|
||||||
style={{
|
style={{
|
||||||
width:
|
width:
|
||||||
Math.min(participants.length, max + 1) * (_size - _overlap) +
|
Math.min(participants.length, max + 1) * (_size - _overlap) +
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, { ReactNode, useCallback, useEffect } from "react";
|
import React, { ReactNode, useCallback, useEffect } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||||
import { LinkButton, Button } from "./button";
|
import { LinkButton, Button } from "./button";
|
||||||
import { useSubmitRageshake } from "./settings/submit-rageshake";
|
import { useSubmitRageshake } from "./settings/submit-rageshake";
|
||||||
import { ErrorMessage } from "./input/Input";
|
import { ErrorMessage } from "./input/Input";
|
||||||
import styles from "./FullScreenView.module.css";
|
import styles from "./FullScreenView.module.css";
|
||||||
|
import { translatedError, TranslatedError } from "./TranslatedError";
|
||||||
|
|
||||||
interface FullScreenViewProps {
|
interface FullScreenViewProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -35,6 +37,7 @@ interface ErrorViewProps {
|
|||||||
|
|
||||||
export function ErrorView({ error }: ErrorViewProps) {
|
export function ErrorView({ error }: ErrorViewProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -47,7 +50,11 @@ export function ErrorView({ error }: ErrorViewProps) {
|
|||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<h1>Error</h1>
|
<h1>Error</h1>
|
||||||
<p>{error.message}</p>
|
<p>
|
||||||
|
{error instanceof TranslatedError
|
||||||
|
? error.translatedMessage
|
||||||
|
: error.message}
|
||||||
|
</p>
|
||||||
{location.pathname === "/" ? (
|
{location.pathname === "/" ? (
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -55,7 +62,7 @@ export function ErrorView({ error }: ErrorViewProps) {
|
|||||||
className={styles.homeLink}
|
className={styles.homeLink}
|
||||||
onPress={onReload}
|
onPress={onReload}
|
||||||
>
|
>
|
||||||
Return to home screen
|
{t("Return to home screen")}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<LinkButton
|
<LinkButton
|
||||||
@@ -64,7 +71,7 @@ export function ErrorView({ error }: ErrorViewProps) {
|
|||||||
className={styles.homeLink}
|
className={styles.homeLink}
|
||||||
to="/"
|
to="/"
|
||||||
>
|
>
|
||||||
Return to home screen
|
{t("Return to home screen")}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
)}
|
)}
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
@@ -72,6 +79,7 @@ export function ErrorView({ error }: ErrorViewProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CrashView() {
|
export function CrashView() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||||
|
|
||||||
const sendDebugLogs = useCallback(() => {
|
const sendDebugLogs = useCallback(() => {
|
||||||
@@ -85,11 +93,11 @@ export function CrashView() {
|
|||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let logsComponent;
|
let logsComponent: JSX.Element | null = null;
|
||||||
if (sent) {
|
if (sent) {
|
||||||
logsComponent = <div>Thanks! We'll get right on it.</div>;
|
logsComponent = <div>{t("Thanks! We'll get right on it.")}</div>;
|
||||||
} else if (sending) {
|
} else if (sending) {
|
||||||
logsComponent = <div>Sending...</div>;
|
logsComponent = <div>{t("Sending…")}</div>;
|
||||||
} else {
|
} else {
|
||||||
logsComponent = (
|
logsComponent = (
|
||||||
<Button
|
<Button
|
||||||
@@ -98,33 +106,39 @@ export function CrashView() {
|
|||||||
onPress={sendDebugLogs}
|
onPress={sendDebugLogs}
|
||||||
className={styles.wideButton}
|
className={styles.wideButton}
|
||||||
>
|
>
|
||||||
Send debug logs
|
{t("Send debug logs")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<h1>Oops, something's gone wrong.</h1>
|
<Trans>
|
||||||
<p>Submitting debug logs will help us track down the problem.</p>
|
<h1>Oops, something's gone wrong.</h1>
|
||||||
|
<p>Submitting debug logs will help us track down the problem.</p>
|
||||||
|
</Trans>
|
||||||
<div className={styles.sendLogsSection}>{logsComponent}</div>
|
<div className={styles.sendLogsSection}>{logsComponent}</div>
|
||||||
{error && <ErrorMessage>Couldn't send debug logs!</ErrorMessage>}
|
{error && (
|
||||||
|
<ErrorMessage error={translatedError("Couldn't send debug logs!", t)} />
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="default"
|
variant="default"
|
||||||
className={styles.wideButton}
|
className={styles.wideButton}
|
||||||
onPress={onReload}
|
onPress={onReload}
|
||||||
>
|
>
|
||||||
Return to home screen
|
{t("Return to home screen")}
|
||||||
</Button>
|
</Button>
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingView() {
|
export function LoadingView() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<h1>Loading...</h1>
|
<h1>{t("Loading…")}</h1>
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { useButton } from "@react-aria/button";
|
import { useButton } from "@react-aria/button";
|
||||||
import { AriaButtonProps } from "@react-types/button";
|
import { AriaButtonProps } from "@react-types/button";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./Header.module.css";
|
import styles from "./Header.module.css";
|
||||||
import { useModalTriggerState } from "./Modal";
|
import { useModalTriggerState } from "./Modal";
|
||||||
@@ -156,6 +157,7 @@ export function VersionMismatchWarning({
|
|||||||
users,
|
users,
|
||||||
room,
|
room,
|
||||||
}: VersionMismatchWarningProps) {
|
}: VersionMismatchWarningProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
|
||||||
const onDetailsClick = useCallback(() => {
|
const onDetailsClick = useCallback(() => {
|
||||||
@@ -166,9 +168,9 @@ export function VersionMismatchWarning({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.versionMismatchWarning}>
|
<span className={styles.versionMismatchWarning}>
|
||||||
Incomaptible versions!
|
{t("Incompatible versions!")}
|
||||||
<Button variant="link" onClick={onDetailsClick}>
|
<Button variant="link" onClick={onDetailsClick}>
|
||||||
Details
|
{t("Details")}
|
||||||
</Button>
|
</Button>
|
||||||
{modalState.isOpen && (
|
{modalState.isOpen && (
|
||||||
<IncompatibleVersionModal userIds={users} room={room} {...modalProps} />
|
<IncompatibleVersionModal userIds={users} room={room} {...modalProps} />
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Modal, ModalContent } from "./Modal";
|
import { Modal, ModalContent } from "./Modal";
|
||||||
import { Body } from "./typography/Typography";
|
import { Body } from "./typography/Typography";
|
||||||
@@ -30,17 +31,21 @@ export const IncompatibleVersionModal: React.FC<Props> = ({
|
|||||||
room,
|
room,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const userLis = Array.from(userIds).map((u) => (
|
const { t } = useTranslation();
|
||||||
<li>{room.getMember(u).name}</li>
|
const userLis = useMemo(
|
||||||
));
|
() => [...userIds].map((u) => <li>{room.getMember(u)?.name ?? u}</li>),
|
||||||
|
[userIds, room]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="Incompatible Versions" isDismissable {...rest}>
|
<Modal title={t("Incompatible versions")} isDismissable {...rest}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<Body>
|
<Body>
|
||||||
Other users are trying to join this call from incompatible versions.
|
<Trans>
|
||||||
These users should ensure that they have refreshed their browsers:
|
Other users are trying to join this call from incompatible versions.
|
||||||
<ul>{userLis}</ul>
|
These users should ensure that they have refreshed their browsers:
|
||||||
|
<ul>{userLis}</ul>
|
||||||
|
</Trans>
|
||||||
</Body>
|
</Body>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { FocusScope } from "@react-aria/focus";
|
|||||||
import { ButtonAria, useButton } from "@react-aria/button";
|
import { ButtonAria, useButton } from "@react-aria/button";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { AriaDialogProps } from "@react-types/dialog";
|
import { AriaDialogProps } from "@react-types/dialog";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||||
import styles from "./Modal.module.css";
|
import styles from "./Modal.module.css";
|
||||||
@@ -53,6 +54,7 @@ export function Modal({
|
|||||||
onClose,
|
onClose,
|
||||||
...rest
|
...rest
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const modalRef = useRef();
|
const modalRef = useRef();
|
||||||
const { overlayProps, underlayProps } = useOverlay(
|
const { overlayProps, underlayProps } = useOverlay(
|
||||||
{ ...rest, onClose },
|
{ ...rest, onClose },
|
||||||
@@ -90,6 +92,7 @@ export function Modal({
|
|||||||
{...closeButtonProps}
|
{...closeButtonProps}
|
||||||
ref={closeButtonRef}
|
ref={closeButtonRef}
|
||||||
className={styles.closeButton}
|
className={styles.closeButton}
|
||||||
|
title={t("Close")}
|
||||||
>
|
>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SequenceDiagramViewer,
|
SequenceDiagramViewer,
|
||||||
@@ -30,7 +31,8 @@ interface DebugLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SequenceDiagramViewerPage() {
|
export function SequenceDiagramViewerPage() {
|
||||||
usePageTitle("Inspector");
|
const { t } = useTranslation();
|
||||||
|
usePageTitle(t("Inspector"));
|
||||||
|
|
||||||
const [debugLog, setDebugLog] = useState<DebugLog>();
|
const [debugLog, setDebugLog] = useState<DebugLog>();
|
||||||
const [selectedUserId, setSelectedUserId] = useState<string>();
|
const [selectedUserId, setSelectedUserId] = useState<string>();
|
||||||
@@ -49,7 +51,7 @@ export function SequenceDiagramViewerPage() {
|
|||||||
type="file"
|
type="file"
|
||||||
id="debugLog"
|
id="debugLog"
|
||||||
name="debugLog"
|
name="debugLog"
|
||||||
label="Debug Log"
|
label={t("Debug log")}
|
||||||
onChange={onChangeDebugLog}
|
onChange={onChangeDebugLog}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 10px;
|
padding: 10px;
|
||||||
color: var(--primary-content);
|
color: var(--primary-content);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 135px;
|
max-width: 135px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
|||||||
targetRef: triggerRef,
|
targetRef: triggerRef,
|
||||||
overlayRef,
|
overlayRef,
|
||||||
isOpen: tooltipState.isOpen,
|
isOpen: tooltipState.isOpen,
|
||||||
offset: 5,
|
offset: 12,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
41
src/TranslatedError.ts
Normal file
41
src/TranslatedError.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
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 i18n from "i18next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error with messages in both English and the user's preferred language.
|
||||||
|
*/
|
||||||
|
// Abstract to force consumers to use the function below rather than calling the
|
||||||
|
// constructor directly
|
||||||
|
export abstract class TranslatedError extends Error {
|
||||||
|
/**
|
||||||
|
* The error message in the user's preferred language.
|
||||||
|
*/
|
||||||
|
public readonly translatedMessage: string;
|
||||||
|
|
||||||
|
public constructor(messageKey: string, translationFn: typeof i18n.t) {
|
||||||
|
super(translationFn(messageKey, { lng: "en-GB" }));
|
||||||
|
this.translatedMessage = translationFn(messageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TranslatedErrorImpl extends TranslatedError {}
|
||||||
|
|
||||||
|
// i18next-parser can't detect calls to a constructor, so we expose a bare
|
||||||
|
// function instead
|
||||||
|
export const translatedError = (messageKey: string, t: typeof i18n.t) =>
|
||||||
|
new TranslatedErrorImpl(messageKey, t);
|
||||||
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export interface RoomParams {
|
export interface UrlParams {
|
||||||
roomAlias: string | null;
|
roomAlias: string | null;
|
||||||
roomId: string | null;
|
roomId: string | null;
|
||||||
viaServers: string[];
|
viaServers: string[];
|
||||||
@@ -29,35 +29,41 @@ export interface RoomParams {
|
|||||||
preload: boolean;
|
preload: boolean;
|
||||||
// Whether to hide the room header when in a call
|
// Whether to hide the room header when in a call
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
|
// Whether to hide the screen-sharing button
|
||||||
|
hideScreensharing: boolean;
|
||||||
// Whether to start a walkie-talkie call instead of a video call
|
// Whether to start a walkie-talkie call instead of a video call
|
||||||
isPtt: boolean;
|
isPtt: boolean;
|
||||||
// Whether to use end-to-end encryption
|
// Whether to use end-to-end encryption
|
||||||
e2eEnabled: boolean;
|
e2eEnabled: boolean;
|
||||||
// The user's ID (only used in Matroska mode)
|
// The user's ID (only used in matryoshka mode)
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
// The display name to use for auto-registration
|
// The display name to use for auto-registration
|
||||||
displayName: string | null;
|
displayName: string | null;
|
||||||
// The device's ID (only used in Matroska mode)
|
// The device's ID (only used in matryoshka mode)
|
||||||
deviceId: string | null;
|
deviceId: string | null;
|
||||||
|
// The base URL of the homeserver to use for media lookups in matryoshka mode
|
||||||
|
baseUrl: string | null;
|
||||||
|
// The BCP 47 code of the language the app should use
|
||||||
|
lang: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the room parameters for the current URL.
|
* Gets the app parameters for the current URL.
|
||||||
* @param {string} query The URL query string
|
* @param query The URL query string
|
||||||
* @param {string} fragment The URL fragment string
|
* @param fragment The URL fragment string
|
||||||
* @returns {RoomParams} The room parameters encoded in the URL
|
* @returns The app parameters encoded in the URL
|
||||||
*/
|
*/
|
||||||
export const getRoomParams = (
|
export const getUrlParams = (
|
||||||
query: string = window.location.search,
|
query: string = window.location.search,
|
||||||
fragment: string = window.location.hash
|
fragment: string = window.location.hash
|
||||||
): RoomParams => {
|
): UrlParams => {
|
||||||
const fragmentQueryStart = fragment.indexOf("?");
|
const fragmentQueryStart = fragment.indexOf("?");
|
||||||
const fragmentParams = new URLSearchParams(
|
const fragmentParams = new URLSearchParams(
|
||||||
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
|
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
|
||||||
);
|
);
|
||||||
const queryParams = new URLSearchParams(query);
|
const queryParams = new URLSearchParams(query);
|
||||||
|
|
||||||
// Normally, room params should be encoded in the fragment so as to avoid
|
// Normally, URL params should be encoded in the fragment so as to avoid
|
||||||
// leaking them to the server. However, we also check the normal query
|
// leaking them to the server. However, we also check the normal query
|
||||||
// string for backwards compatibility with versions that only used that.
|
// string for backwards compatibility with versions that only used that.
|
||||||
const hasParam = (name: string): boolean =>
|
const hasParam = (name: string): boolean =>
|
||||||
@@ -82,19 +88,22 @@ export const getRoomParams = (
|
|||||||
isEmbedded: hasParam("embed"),
|
isEmbedded: hasParam("embed"),
|
||||||
preload: hasParam("preload"),
|
preload: hasParam("preload"),
|
||||||
hideHeader: hasParam("hideHeader"),
|
hideHeader: hasParam("hideHeader"),
|
||||||
|
hideScreensharing: hasParam("hideScreensharing"),
|
||||||
isPtt: hasParam("ptt"),
|
isPtt: hasParam("ptt"),
|
||||||
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
||||||
userId: getParam("userId"),
|
userId: getParam("userId"),
|
||||||
displayName: getParam("displayName"),
|
displayName: getParam("displayName"),
|
||||||
deviceId: getParam("deviceId"),
|
deviceId: getParam("deviceId"),
|
||||||
|
baseUrl: getParam("baseUrl"),
|
||||||
|
lang: getParam("lang"),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to simplify use of getRoomParams.
|
* Hook to simplify use of getUrlParams.
|
||||||
* @returns {RoomParams} The room parameters for the current URL
|
* @returns The app parameters for the current URL
|
||||||
*/
|
*/
|
||||||
export const useRoomParams = (): RoomParams => {
|
export const useUrlParams = (): UrlParams => {
|
||||||
const { hash, search } = useLocation();
|
const { hash, search } = useLocation();
|
||||||
return useMemo(() => getRoomParams(search, hash), [search, hash]);
|
return useMemo(() => getUrlParams(search, hash), [search, hash]);
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button, LinkButton } from "./button";
|
import { Button, LinkButton } from "./button";
|
||||||
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
|
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
|
||||||
@@ -30,6 +31,7 @@ export function UserMenu({
|
|||||||
avatarUrl,
|
avatarUrl,
|
||||||
onAction,
|
onAction,
|
||||||
}: UserMenuProps) {
|
}: UserMenuProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
@@ -45,7 +47,7 @@ export function UserMenu({
|
|||||||
if (isPasswordlessUser && !preventNavigation) {
|
if (isPasswordlessUser && !preventNavigation) {
|
||||||
arr.push({
|
arr.push({
|
||||||
key: "login",
|
key: "login",
|
||||||
label: "Sign In",
|
label: t("Sign in"),
|
||||||
icon: LoginIcon,
|
icon: LoginIcon,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -53,14 +55,16 @@ export function UserMenu({
|
|||||||
if (!isPasswordlessUser && !preventNavigation) {
|
if (!isPasswordlessUser && !preventNavigation) {
|
||||||
arr.push({
|
arr.push({
|
||||||
key: "logout",
|
key: "logout",
|
||||||
label: "Sign Out",
|
label: t("Sign out"),
|
||||||
icon: LogoutIcon,
|
icon: LogoutIcon,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return arr;
|
return arr;
|
||||||
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation]);
|
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation, t]);
|
||||||
|
|
||||||
|
const tooltip = useCallback(() => t("Profile"), [t]);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
@@ -72,7 +76,7 @@ export function UserMenu({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverMenuTrigger placement="bottom right">
|
<PopoverMenuTrigger placement="bottom right">
|
||||||
<TooltipTrigger tooltip={() => "Profile"} placement="bottom left">
|
<TooltipTrigger tooltip={tooltip} placement="bottom left">
|
||||||
<Button variant="icon" className={styles.userButton}>
|
<Button variant="icon" className={styles.userButton}>
|
||||||
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -87,7 +91,7 @@ export function UserMenu({
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="User menu" onAction={onAction}>
|
<Menu {...props} label={t("User menu")} onAction={onAction}>
|
||||||
{items.map(({ key, icon: Icon, label }) => (
|
{items.map(({ key, icon: Icon, label }) => (
|
||||||
<Item key={key} textValue={label}>
|
<Item key={key} textValue={label}>
|
||||||
<Icon width={24} height={24} className={styles.menuIcon} />
|
<Icon width={24} height={24} className={styles.menuIcon} />
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
@@ -34,7 +35,8 @@ import { useInteractiveLogin } from "./useInteractiveLogin";
|
|||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export const LoginPage: FC = () => {
|
export const LoginPage: FC = () => {
|
||||||
usePageTitle("Login");
|
const { t } = useTranslation();
|
||||||
|
usePageTitle(t("Login"));
|
||||||
|
|
||||||
const { setClient } = useClient();
|
const { setClient } = useClient();
|
||||||
const login = useInteractiveLogin();
|
const login = useInteractiveLogin();
|
||||||
@@ -93,8 +95,8 @@ export const LoginPage: FC = () => {
|
|||||||
<InputField
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
placeholder="Username"
|
placeholder={t("Username")}
|
||||||
label="Username"
|
label={t("Username")}
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
prefix="@"
|
prefix="@"
|
||||||
@@ -105,18 +107,18 @@ export const LoginPage: FC = () => {
|
|||||||
<InputField
|
<InputField
|
||||||
type="password"
|
type="password"
|
||||||
ref={passwordRef}
|
ref={passwordRef}
|
||||||
placeholder="Password"
|
placeholder={t("Password")}
|
||||||
label="Password"
|
label={t("Password")}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button type="submit" disabled={loading}>
|
<Button type="submit" disabled={loading}>
|
||||||
{loading ? "Logging in..." : "Login"}
|
{loading ? t("Logging in…") : t("Login")}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</form>
|
</form>
|
||||||
@@ -124,9 +126,11 @@ export const LoginPage: FC = () => {
|
|||||||
<div className={styles.authLinks}>
|
<div className={styles.authLinks}>
|
||||||
<p>Not registered yet?</p>
|
<p>Not registered yet?</p>
|
||||||
<p>
|
<p>
|
||||||
<Link to="/register">Create an account</Link>
|
<Trans>
|
||||||
{" Or "}
|
<Link to="/register">Create an account</Link>
|
||||||
<Link to="/">Access as a guest</Link>
|
{" Or "}
|
||||||
|
<Link to="/">Access as a guest</Link>
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import React, {
|
|||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import { captureException } from "@sentry/react";
|
import { captureException } from "@sentry/react";
|
||||||
import { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -40,7 +41,8 @@ import { Caption, Link } from "../typography/Typography";
|
|||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export const RegisterPage: FC = () => {
|
export const RegisterPage: FC = () => {
|
||||||
usePageTitle("Register");
|
const { t } = useTranslation();
|
||||||
|
usePageTitle(t("Register"));
|
||||||
|
|
||||||
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
|
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
|
||||||
useClient();
|
useClient();
|
||||||
@@ -126,11 +128,11 @@ export const RegisterPage: FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
if (password && passwordConfirmation && password !== passwordConfirmation) {
|
||||||
confirmPasswordRef.current?.setCustomValidity("Passwords must match");
|
confirmPasswordRef.current?.setCustomValidity(t("Passwords must match"));
|
||||||
} else {
|
} else {
|
||||||
confirmPasswordRef.current?.setCustomValidity("");
|
confirmPasswordRef.current?.setCustomValidity("");
|
||||||
}
|
}
|
||||||
}, [password, passwordConfirmation]);
|
}, [password, passwordConfirmation, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
|
||||||
@@ -154,8 +156,8 @@ export const RegisterPage: FC = () => {
|
|||||||
<InputField
|
<InputField
|
||||||
type="text"
|
type="text"
|
||||||
name="userName"
|
name="userName"
|
||||||
placeholder="Username"
|
placeholder={t("Username")}
|
||||||
label="Username"
|
label={t("Username")}
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
prefix="@"
|
prefix="@"
|
||||||
@@ -171,8 +173,8 @@ export const RegisterPage: FC = () => {
|
|||||||
setPassword(e.target.value)
|
setPassword(e.target.value)
|
||||||
}
|
}
|
||||||
value={password}
|
value={password}
|
||||||
placeholder="Password"
|
placeholder={t("Password")}
|
||||||
label="Password"
|
label={t("Password")}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
@@ -184,45 +186,49 @@ export const RegisterPage: FC = () => {
|
|||||||
setPasswordConfirmation(e.target.value)
|
setPasswordConfirmation(e.target.value)
|
||||||
}
|
}
|
||||||
value={passwordConfirmation}
|
value={passwordConfirmation}
|
||||||
placeholder="Confirm Password"
|
placeholder={t("Confirm password")}
|
||||||
label="Confirm Password"
|
label={t("Confirm password")}
|
||||||
ref={confirmPasswordRef}
|
ref={confirmPasswordRef}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Caption>
|
<Caption>
|
||||||
This site is protected by ReCAPTCHA and the Google{" "}
|
<Trans>
|
||||||
<Link href="https://www.google.com/policies/privacy/">
|
This site is protected by ReCAPTCHA and the Google{" "}
|
||||||
Privacy Policy
|
<Link href="https://www.google.com/policies/privacy/">
|
||||||
</Link>{" "}
|
Privacy Policy
|
||||||
and{" "}
|
</Link>{" "}
|
||||||
<Link href="https://policies.google.com/terms">
|
and{" "}
|
||||||
Terms of Service
|
<Link href="https://policies.google.com/terms">
|
||||||
</Link>{" "}
|
Terms of Service
|
||||||
apply.
|
</Link>{" "}
|
||||||
<br />
|
apply.
|
||||||
By clicking "Register", you agree to our{" "}
|
<br />
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
By clicking "Register", you agree to our{" "}
|
||||||
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
|
</Trans>
|
||||||
</Caption>
|
</Caption>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button type="submit" disabled={registering}>
|
<Button type="submit" disabled={registering}>
|
||||||
{registering ? "Registering..." : "Register"}
|
{registering ? t("Registering…") : t("Register")}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<div id={recaptchaId} />
|
<div id={recaptchaId} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.authLinks}>
|
<div className={styles.authLinks}>
|
||||||
<p>Already have an account?</p>
|
<Trans>
|
||||||
<p>
|
<p>Already have an account?</p>
|
||||||
<Link to="/login">Log in</Link>
|
<p>
|
||||||
{" Or "}
|
<Link to="/login">Log in</Link>
|
||||||
<Link to="/">Access as a guest</Link>
|
{" Or "}
|
||||||
</p>
|
<Link to="/">Access as a guest</Link>
|
||||||
|
</p>
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const useInteractiveLogin = () =>
|
|||||||
password: string
|
password: string
|
||||||
) => Promise<[MatrixClient, Session]>
|
) => Promise<[MatrixClient, Session]>
|
||||||
>(async (homeserver: string, username: string, password: string) => {
|
>(async (homeserver: string, username: string, password: string) => {
|
||||||
const authClient = createClient(homeserver);
|
const authClient = createClient({ baseUrl: homeserver });
|
||||||
|
|
||||||
const interactiveAuth = new InteractiveAuth({
|
const interactiveAuth = new InteractiveAuth({
|
||||||
matrixClient: authClient,
|
matrixClient: authClient,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const useInteractiveRegistration = (): [
|
|||||||
|
|
||||||
const authClient = useRef<MatrixClient>();
|
const authClient = useRef<MatrixClient>();
|
||||||
if (!authClient.current) {
|
if (!authClient.current) {
|
||||||
authClient.current = createClient(defaultHomeserver);
|
authClient.current = createClient({ baseUrl: defaultHomeserver });
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ limitations under the License.
|
|||||||
|
|
||||||
import { useEffect, useCallback, useRef, useState } from "react";
|
import { useEffect, useCallback, useRef, useState } from "react";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { translatedError } from "../TranslatedError";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -32,6 +35,7 @@ interface RecaptchaPromiseRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useRecaptcha = (sitekey: string) => {
|
export const useRecaptcha = (sitekey: string) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [recaptchaId] = useState(() => randomString(16));
|
const [recaptchaId] = useState(() => randomString(16));
|
||||||
const promiseRef = useRef<RecaptchaPromiseRef>();
|
const promiseRef = useRef<RecaptchaPromiseRef>();
|
||||||
|
|
||||||
@@ -71,14 +75,14 @@ export const useRecaptcha = (sitekey: string) => {
|
|||||||
|
|
||||||
if (!window.grecaptcha) {
|
if (!window.grecaptcha) {
|
||||||
console.log("Recaptcha not loaded");
|
console.log("Recaptcha not loaded");
|
||||||
return Promise.reject(new Error("Recaptcha not loaded"));
|
return Promise.reject(translatedError("Recaptcha not loaded", t));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const observer = new MutationObserver((mutationsList) => {
|
const observer = new MutationObserver((mutationsList) => {
|
||||||
for (const item of mutationsList) {
|
for (const item of mutationsList) {
|
||||||
if ((item.target as HTMLElement)?.style?.visibility !== "visible") {
|
if ((item.target as HTMLElement)?.style?.visibility !== "visible") {
|
||||||
reject(new Error("Recaptcha dismissed"));
|
reject(translatedError("Recaptcha dismissed", t));
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -108,7 +112,7 @@ export const useRecaptcha = (sitekey: string) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [sitekey]);
|
}, [sitekey, t]);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
window.grecaptcha?.reset();
|
window.grecaptcha?.reset();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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";
|
||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
|
||||||
@@ -142,9 +143,11 @@ export function MicButton({
|
|||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
tooltip={() => (muted ? "Unmute microphone" : "Mute microphone")}
|
tooltip={() => (muted ? t("Unmute microphone") : t("Mute microphone"))}
|
||||||
>
|
>
|
||||||
<Button variant="toolbar" {...rest} off={muted}>
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||||
@@ -161,9 +164,11 @@ export function VideoButton({
|
|||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
tooltip={() => (muted ? "Turn on camera" : "Turn off camera")}
|
tooltip={() => (muted ? t("Turn on camera") : t("Turn off camera"))}
|
||||||
>
|
>
|
||||||
<Button variant="toolbar" {...rest} off={muted}>
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||||
@@ -182,9 +187,11 @@ export function ScreenshareButton({
|
|||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
tooltip={() => (enabled ? "Stop sharing screen" : "Share screen")}
|
tooltip={() => (enabled ? t("Stop sharing screen") : t("Share screen"))}
|
||||||
>
|
>
|
||||||
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
||||||
<ScreenshareIcon />
|
<ScreenshareIcon />
|
||||||
@@ -201,8 +208,11 @@ export function HangupButton({
|
|||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Leave"), [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger tooltip={() => "Leave"}>
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
<Button
|
<Button
|
||||||
variant="toolbar"
|
variant="toolbar"
|
||||||
className={classNames(styles.hangupButton, className)}
|
className={classNames(styles.hangupButton, className)}
|
||||||
@@ -222,8 +232,11 @@ export function SettingsButton({
|
|||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Settings"), [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger tooltip={() => "Settings"}>
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
<Button variant="toolbar" {...rest}>
|
<Button variant="toolbar" {...rest}>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -239,8 +252,11 @@ export function InviteButton({
|
|||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Invite"), [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger tooltip={() => "Invite"}>
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
<Button variant="toolbar" {...rest}>
|
<Button variant="toolbar" {...rest}>
|
||||||
<AddUserIcon />
|
<AddUserIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -256,8 +272,11 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Local volume"), [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger tooltip={() => "Local volume"}>
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
<Button variant="icon" {...rest}>
|
<Button variant="icon" {...rest}>
|
||||||
<VolumeIcon volume={volume} />
|
<VolumeIcon volume={volume} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -273,12 +292,13 @@ export function FullscreenButton({
|
|||||||
fullscreen,
|
fullscreen,
|
||||||
...rest
|
...rest
|
||||||
}: FullscreenButtonProps) {
|
}: FullscreenButtonProps) {
|
||||||
const getTooltip = useCallback(() => {
|
const { t } = useTranslation();
|
||||||
return fullscreen ? "Exit full screen" : "Full screen";
|
const tooltip = useCallback(() => {
|
||||||
}, [fullscreen]);
|
return fullscreen ? t("Exit full screen") : t("Full screen");
|
||||||
|
}, [fullscreen, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger tooltip={getTooltip}>
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
<Button variant="icon" {...rest}>
|
<Button variant="icon" {...rest}>
|
||||||
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
|
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import useClipboard from "react-use-clipboard";
|
import useClipboard from "react-use-clipboard";
|
||||||
|
|
||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||||
@@ -36,6 +37,7 @@ export function CopyButton({
|
|||||||
copiedMessage,
|
copiedMessage,
|
||||||
...rest
|
...rest
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -49,7 +51,7 @@ export function CopyButton({
|
|||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<>
|
<>
|
||||||
{variant !== "icon" && <span>{copiedMessage || "Copied!"}</span>}
|
{variant !== "icon" && <span>{copiedMessage || t("Copied!")}</span>}
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Headline } from "../typography/Typography";
|
import { Headline } from "../typography/Typography";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -39,25 +40,29 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
|
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverMenuTrigger placement="bottom">
|
<PopoverMenuTrigger placement="bottom">
|
||||||
<Button variant="dropdown" className={commonStyles.headline}>
|
<Button variant="dropdown" className={commonStyles.headline}>
|
||||||
<Headline className={styles.label}>
|
<Headline className={styles.label}>
|
||||||
{callType === CallType.Video ? "Video call" : "Walkie-talkie call"}
|
{callType === CallType.Video
|
||||||
|
? t("Video call")
|
||||||
|
: t("Walkie-talkie call")}
|
||||||
</Headline>
|
</Headline>
|
||||||
</Button>
|
</Button>
|
||||||
{(props: JSX.IntrinsicAttributes) => (
|
{(props: JSX.IntrinsicAttributes) => (
|
||||||
<Menu {...props} label="Call type menu" onAction={setCallType}>
|
<Menu {...props} label={t("Call type menu")} onAction={setCallType}>
|
||||||
<Item key={CallType.Video} textValue="Video call">
|
<Item key={CallType.Video} textValue={t("Video call")}>
|
||||||
<VideoIcon />
|
<VideoIcon />
|
||||||
<span>Video call</span>
|
<span>{t("Video call")}</span>
|
||||||
{callType === CallType.Video && (
|
{callType === CallType.Video && (
|
||||||
<CheckIcon className={menuStyles.checkIcon} />
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
)}
|
)}
|
||||||
</Item>
|
</Item>
|
||||||
<Item key={CallType.Radio} textValue="Walkie-talkie call">
|
<Item key={CallType.Radio} textValue={t("Walkie-talkie call")}>
|
||||||
<MicIcon />
|
<MicIcon />
|
||||||
<span>Walkie-talkie call</span>
|
<span>{t("Walkie-talkie call")}</span>
|
||||||
{callType === CallType.Radio && (
|
{callType === CallType.Radio && (
|
||||||
<CheckIcon className={menuStyles.checkIcon} />
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
@@ -23,7 +24,8 @@ import { RegisteredView } from "./RegisteredView";
|
|||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
usePageTitle("Home");
|
const { t } = useTranslation();
|
||||||
|
usePageTitle(t("Home"));
|
||||||
|
|
||||||
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
|
||||||
useClient();
|
useClient();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { PressEvent } from "@react-types/shared";
|
import { PressEvent } from "@react-types/shared";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -29,13 +30,15 @@ interface Props {
|
|||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}
|
}
|
||||||
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
|
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="Join existing call?" isDismissable {...rest}>
|
<Modal title={t("Join existing call?")} isDismissable {...rest}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<p>This call already exists, would you like to join?</p>
|
<p>{t("This call already exists, would you like to join?")}</p>
|
||||||
<FieldRow rightAlign className={styles.buttons}>
|
<FieldRow rightAlign className={styles.buttons}>
|
||||||
<Button onPress={onClose}>No</Button>
|
<Button onPress={onClose}>{t("No")}</Button>
|
||||||
<Button onPress={onJoin}>Yes, join call</Button>
|
<Button onPress={onJoin}>{t("Yes, join call")}</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
|
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
|
||||||
import { useGroupCallRooms } from "./useGroupCallRooms";
|
import { useGroupCallRooms } from "./useGroupCallRooms";
|
||||||
@@ -48,6 +49,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<Error>();
|
const [error, setError] = useState<Error>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { t } = useTranslation();
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
|
||||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||||
@@ -93,7 +95,9 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
|||||||
}, [history, existingRoomId]);
|
}, [history, existingRoomId]);
|
||||||
|
|
||||||
const callNameLabel =
|
const callNameLabel =
|
||||||
callType === CallType.Video ? "Video call name" : "Walkie-talkie call name";
|
callType === CallType.Video
|
||||||
|
? t("Video call name")
|
||||||
|
: t("Walkie-talkie call name");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -127,19 +131,19 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
|||||||
className={styles.button}
|
className={styles.button}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? "Loading..." : "Go"}
|
{loading ? t("Loading…") : t("Go")}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow className={styles.fieldRow}>
|
<FieldRow className={styles.fieldRow}>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
{recentRooms.length > 0 && (
|
{recentRooms.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Title className={styles.recentCallsTitle}>
|
<Title className={styles.recentCallsTitle}>
|
||||||
Your recent Calls
|
{t("Your recent calls")}
|
||||||
</Title>
|
</Title>
|
||||||
<CallList rooms={recentRooms} client={client} disableFacepile />
|
<CallList rooms={recentRooms} client={client} disableFacepile />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import React, { FC, useCallback, useState, FormEventHandler } from "react";
|
import React, { FC, useCallback, useState, FormEventHandler } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||||
@@ -47,6 +48,7 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
const [onFinished, setOnFinished] = useState<() => void>();
|
const [onFinished, setOnFinished] = useState<() => void>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -105,7 +107,9 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const callNameLabel =
|
const callNameLabel =
|
||||||
callType === CallType.Video ? "Video call name" : "Walkie-talkie call name";
|
callType === CallType.Video
|
||||||
|
? t("Video call name")
|
||||||
|
: t("Walkie-talkie call name");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -137,24 +141,26 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
<InputField
|
<InputField
|
||||||
id="displayName"
|
id="displayName"
|
||||||
name="displayName"
|
name="displayName"
|
||||||
label="Display Name"
|
label={t("Display name")}
|
||||||
placeholder="Display Name"
|
placeholder={t("Display name")}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Caption>
|
<Caption>
|
||||||
By clicking "Go", you agree to our{" "}
|
<Trans>
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
By clicking "Go", you agree to our{" "}
|
||||||
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
|
</Trans>
|
||||||
</Caption>
|
</Caption>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" size="lg" disabled={loading}>
|
<Button type="submit" size="lg" disabled={loading}>
|
||||||
{loading ? "Loading..." : "Go"}
|
{loading ? t("Loading…") : t("Go")}
|
||||||
</Button>
|
</Button>
|
||||||
<div id={recaptchaId} />
|
<div id={recaptchaId} />
|
||||||
</Form>
|
</Form>
|
||||||
@@ -162,14 +168,16 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
<Body className={styles.mobileLoginLink}>
|
<Body className={styles.mobileLoginLink}>
|
||||||
<Link color="primary" to="/login">
|
<Link color="primary" to="/login">
|
||||||
Login to your account
|
{t("Login to your account")}
|
||||||
</Link>
|
</Link>
|
||||||
</Body>
|
</Body>
|
||||||
<Body>
|
<Body>
|
||||||
Not registered yet?{" "}
|
<Trans>
|
||||||
<Link color="primary" to="/register">
|
Not registered yet?{" "}
|
||||||
Create an account
|
<Link color="primary" to="/register">
|
||||||
</Link>
|
Create an account
|
||||||
|
</Link>
|
||||||
|
</Trans>
|
||||||
</Body>
|
</Body>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useCallback } from "react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Avatar, Size } from "../Avatar";
|
import { Avatar, Size } from "../Avatar";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -39,6 +40,8 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
|||||||
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
|
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [removed, setRemoved] = useState(false);
|
const [removed, setRemoved] = useState(false);
|
||||||
const [objUrl, setObjUrl] = useState<string>(null);
|
const [objUrl, setObjUrl] = useState<string>(null);
|
||||||
|
|
||||||
@@ -97,7 +100,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
|||||||
variant="icon"
|
variant="icon"
|
||||||
onPress={onPressRemoveAvatar}
|
onPress={onPressRemoveAvatar}
|
||||||
>
|
>
|
||||||
Remove
|
{t("Remove")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ChangeEvent, forwardRef, ReactNode } from "react";
|
import React, { ChangeEvent, FC, forwardRef, ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import styles from "./Input.module.css";
|
import styles from "./Input.module.css";
|
||||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||||
|
import { TranslatedError } from "../TranslatedError";
|
||||||
|
|
||||||
interface FieldRowProps {
|
interface FieldRowProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -140,10 +141,12 @@ export const InputField = forwardRef<
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export function ErrorMessage({
|
interface ErrorMessageProps {
|
||||||
children,
|
error: Error;
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
}): JSX.Element {
|
|
||||||
return <p className={styles.errorMessage}>{children}</p>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ErrorMessage: FC<ErrorMessageProps> = ({ error }) => (
|
||||||
|
<p className={styles.errorMessage}>
|
||||||
|
{error instanceof TranslatedError ? error.translatedMessage : error.message}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { AriaSelectOptions, HiddenSelect, useSelect } from "@react-aria/select";
|
|||||||
import { useButton } from "@react-aria/button";
|
import { useButton } from "@react-aria/button";
|
||||||
import { useSelectState } from "@react-stately/select";
|
import { useSelectState } from "@react-stately/select";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Popover } from "../popover/Popover";
|
import { Popover } from "../popover/Popover";
|
||||||
import { ListBox } from "../ListBox";
|
import { ListBox } from "../ListBox";
|
||||||
@@ -30,6 +31,7 @@ interface Props extends AriaSelectOptions<object> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SelectInput(props: Props): JSX.Element {
|
export function SelectInput(props: Props): JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
const state = useSelectState(props);
|
const state = useSelectState(props);
|
||||||
|
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
@@ -56,7 +58,7 @@ export function SelectInput(props: Props): JSX.Element {
|
|||||||
<span {...valueProps} className={styles.selectedItem}>
|
<span {...valueProps} className={styles.selectedItem}>
|
||||||
{state.selectedItem
|
{state.selectedItem
|
||||||
? state.selectedItem.rendered
|
? state.selectedItem.rendered
|
||||||
: "Select an option"}
|
: t("Select an option")}
|
||||||
</span>
|
</span>
|
||||||
<ArrowDownIcon />
|
<ArrowDownIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
34
src/main.tsx
34
src/main.tsx
@@ -25,10 +25,15 @@ import ReactDOM from "react-dom";
|
|||||||
import { createBrowserHistory } from "history";
|
import { createBrowserHistory } from "history";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { Integrations } from "@sentry/tracing";
|
import { Integrations } from "@sentry/tracing";
|
||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
import Backend from "i18next-http-backend";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { init as initRageshake } from "./settings/rageshake";
|
import { init as initRageshake } from "./settings/rageshake";
|
||||||
|
import { getUrlParams } from "./UrlParams";
|
||||||
|
|
||||||
initRageshake();
|
initRageshake();
|
||||||
|
|
||||||
@@ -104,6 +109,35 @@ Sentry.init({
|
|||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const languageDetector = new LanguageDetector();
|
||||||
|
languageDetector.addDetector({
|
||||||
|
name: "urlFragment",
|
||||||
|
// Look for a language code in the URL's fragment
|
||||||
|
lookup: () => getUrlParams().lang ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(Backend)
|
||||||
|
.use(languageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: "en-GB",
|
||||||
|
defaultNS: "app",
|
||||||
|
keySeparator: false,
|
||||||
|
nsSeparator: false,
|
||||||
|
pluralSeparator: "|",
|
||||||
|
contextSeparator: "|",
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // React has built-in XSS protections
|
||||||
|
},
|
||||||
|
detection: {
|
||||||
|
// No localStorage detectors or caching here, since we don't have any way
|
||||||
|
// of letting the user manually select a language
|
||||||
|
order: ["urlFragment", "navigator"],
|
||||||
|
caches: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App history={history} />
|
<App history={history} />
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||||
import { getRoomParams } from "./room/useRoomParams";
|
import { getUrlParams } from "./UrlParams";
|
||||||
|
|
||||||
export const defaultHomeserver =
|
export const defaultHomeserver =
|
||||||
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
|
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
|
||||||
@@ -134,12 +134,12 @@ export async function initClient(
|
|||||||
storeOpts.cryptoStore = new MemoryCryptoStore();
|
storeOpts.cryptoStore = new MemoryCryptoStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: we read from the room params in RoomPage too:
|
// XXX: we read from the URL params in RoomPage too:
|
||||||
// it would be much better to read them in one place and pass
|
// it would be much better to read them in one place and pass
|
||||||
// the values around, but we initialise the matrix client in
|
// the values around, but we initialise the matrix client in
|
||||||
// many different places so we'd have to pass it into all of
|
// many different places so we'd have to pass it into all of
|
||||||
// them.
|
// them.
|
||||||
const { e2eEnabled } = getRoomParams();
|
const { e2eEnabled } = getUrlParams();
|
||||||
if (!e2eEnabled) {
|
if (!e2eEnabled) {
|
||||||
logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
|
logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
|
||||||
}
|
}
|
||||||
|
|||||||
71
src/media-utils.ts
Normal file
71
src/media-utils.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a media device with label matching 'deviceName'
|
||||||
|
* @param deviceName The label of the device to look for
|
||||||
|
* @param devices The list of devices to search
|
||||||
|
* @returns A matching media device or undefined if no matching device was found
|
||||||
|
*/
|
||||||
|
export async function findDeviceByName(
|
||||||
|
deviceName: string,
|
||||||
|
kind: MediaDeviceKind,
|
||||||
|
devices: MediaDeviceInfo[]
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const deviceInfo = devices.find(
|
||||||
|
(d) => d.kind === kind && d.label === deviceName
|
||||||
|
);
|
||||||
|
return deviceInfo?.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the available audio input/output and video input devices
|
||||||
|
* from the browser: a wrapper around mediaDevices.enumerateDevices()
|
||||||
|
* that requests a stream and holds it while calling enumerateDevices().
|
||||||
|
* This is because some browsers (Firefox) only return device labels when
|
||||||
|
* the app has an active user media stream. In Chrome, this will get a
|
||||||
|
* stream from the default camera which can mean, for example, that the
|
||||||
|
* light for the FaceTime camera turns on briefly even if you selected
|
||||||
|
* another camera. Once the Permissions API
|
||||||
|
* (https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API)
|
||||||
|
* is ready for primetime, this should allow us to avoid this.
|
||||||
|
*
|
||||||
|
* @return The available media devices
|
||||||
|
*/
|
||||||
|
export async function getDevices(): Promise<MediaDeviceInfo[]> {
|
||||||
|
let stream: MediaStream;
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.info("Couldn't get media stream for enumerateDevices: failing");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await navigator.mediaDevices.enumerateDevices();
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("Unable to refresh WebRTC Devices: ", error);
|
||||||
|
} finally {
|
||||||
|
for (const track of stream.getTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, { ChangeEvent, useCallback, useEffect, useState } from "react";
|
import React, { ChangeEvent, useCallback, useEffect, useState } from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useProfile } from "./useProfile";
|
import { useProfile } from "./useProfile";
|
||||||
@@ -31,6 +32,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export function ProfileModal({ client, ...rest }: Props) {
|
export function ProfileModal({ client, ...rest }: Props) {
|
||||||
const { onClose } = rest;
|
const { onClose } = rest;
|
||||||
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
success,
|
success,
|
||||||
error,
|
error,
|
||||||
@@ -83,14 +85,14 @@ export function ProfileModal({ client, ...rest }: Props) {
|
|||||||
}, [success, onClose]);
|
}, [success, onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="Profile" isDismissable {...rest}>
|
<Modal title={t("Profile")} isDismissable {...rest}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<FieldRow className={styles.avatarFieldRow}>
|
<FieldRow className={styles.avatarFieldRow}>
|
||||||
<AvatarInputField
|
<AvatarInputField
|
||||||
id="avatar"
|
id="avatar"
|
||||||
name="avatar"
|
name="avatar"
|
||||||
label="Avatar"
|
label={t("Avatar")}
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
onRemoveAvatar={onRemoveAvatar}
|
onRemoveAvatar={onRemoveAvatar}
|
||||||
@@ -100,7 +102,7 @@ export function ProfileModal({ client, ...rest }: Props) {
|
|||||||
<InputField
|
<InputField
|
||||||
id="userId"
|
id="userId"
|
||||||
name="userId"
|
name="userId"
|
||||||
label="User Id"
|
label={t("User ID")}
|
||||||
type="text"
|
type="text"
|
||||||
disabled
|
disabled
|
||||||
value={client.getUserId()}
|
value={client.getUserId()}
|
||||||
@@ -110,18 +112,18 @@ export function ProfileModal({ client, ...rest }: Props) {
|
|||||||
<InputField
|
<InputField
|
||||||
id="displayName"
|
id="displayName"
|
||||||
name="displayName"
|
name="displayName"
|
||||||
label="Display Name"
|
label={t("Display name")}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="Display Name"
|
placeholder={t("Display name")}
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={onChangeDisplayName}
|
onChange={onChangeDisplayName}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
<FieldRow rightAlign>
|
<FieldRow rightAlign>
|
||||||
@@ -129,7 +131,7 @@ export function ProfileModal({ client, ...rest }: Props) {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={loading}>
|
<Button type="submit" disabled={loading}>
|
||||||
{loading ? "Saving..." : "Save"}
|
{loading ? t("Saving…") : t("Save")}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export function useProfile(client: MatrixClient) {
|
|||||||
if (removeAvatar) {
|
if (removeAvatar) {
|
||||||
await client.setAvatarUrl("");
|
await client.setAvatarUrl("");
|
||||||
} else if (avatar) {
|
} else if (avatar) {
|
||||||
mxcAvatarUrl = await client.uploadContent(avatar);
|
({ content_uri: mxcAvatarUrl } = await client.uploadContent(
|
||||||
|
avatar
|
||||||
|
));
|
||||||
await client.setAvatarUrl(mxcAvatarUrl);
|
await client.setAvatarUrl(mxcAvatarUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./AudioPreview.module.css";
|
import styles from "./AudioPreview.module.css";
|
||||||
import { SelectInput } from "../input/SelectInput";
|
import { SelectInput } from "../input/SelectInput";
|
||||||
@@ -43,24 +44,26 @@ export function AudioPreview({
|
|||||||
audioOutputs,
|
audioOutputs,
|
||||||
setAudioOutput,
|
setAudioOutput,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>{`${roomName} - Walkie-talkie call`}</h1>
|
<h1>{t("{{roomName}} - Walkie-talkie call", { roomName })}</h1>
|
||||||
<div className={styles.preview}>
|
<div className={styles.preview}>
|
||||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||||
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
|
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
|
||||||
Microphone permissions needed to join the call.
|
{t("Microphone permissions needed to join the call.")}
|
||||||
</Body>
|
</Body>
|
||||||
)}
|
)}
|
||||||
{state === GroupCallState.InitializingLocalCallFeed && (
|
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||||
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
|
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
|
||||||
Accept microphone permissions to join the call.
|
{t("Accept microphone permissions to join the call.")}
|
||||||
</Body>
|
</Body>
|
||||||
)}
|
)}
|
||||||
{state === GroupCallState.LocalCallFeedInitialized && (
|
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||||
<>
|
<>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label="Microphone"
|
label={t("Microphone")}
|
||||||
selectedKey={audioInput}
|
selectedKey={audioInput}
|
||||||
onSelectionChange={setAudioInput}
|
onSelectionChange={setAudioInput}
|
||||||
className={styles.inputField}
|
className={styles.inputField}
|
||||||
@@ -69,13 +72,13 @@ export function AudioPreview({
|
|||||||
<Item key={deviceId}>
|
<Item key={deviceId}>
|
||||||
{!!label && label.trim().length > 0
|
{!!label && label.trim().length > 0
|
||||||
? label
|
? label
|
||||||
: `Microphone ${index + 1}`}
|
: t("Microphone {{n}}", { n: index + 1 })}
|
||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</SelectInput>
|
</SelectInput>
|
||||||
{audioOutputs.length > 0 && (
|
{audioOutputs.length > 0 && (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label="Speaker"
|
label={t("Speaker")}
|
||||||
selectedKey={audioOutput}
|
selectedKey={audioOutput}
|
||||||
onSelectionChange={setAudioOutput}
|
onSelectionChange={setAudioOutput}
|
||||||
className={styles.inputField}
|
className={styles.inputField}
|
||||||
@@ -84,7 +87,7 @@ export function AudioPreview({
|
|||||||
<Item key={deviceId}>
|
<Item key={deviceId}>
|
||||||
{!!label && label.trim().length > 0
|
{!!label && label.trim().length > 0
|
||||||
? label
|
? label
|
||||||
: `Speaker ${index + 1}`}
|
: t("Speaker {{n}}", { n: index + 1 })}
|
||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</SelectInput>
|
</SelectInput>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./CallEndedView.module.css";
|
import styles from "./CallEndedView.module.css";
|
||||||
import { LinkButton } from "../button";
|
import { LinkButton } from "../button";
|
||||||
@@ -24,6 +25,7 @@ import { Subtitle, Body, Link, Headline } from "../typography/Typography";
|
|||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||||
|
|
||||||
export function CallEndedView({ client }: { client: MatrixClient }) {
|
export function CallEndedView({ client }: { client: MatrixClient }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { displayName } = useProfile(client);
|
const { displayName } = useProfile(client);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,29 +39,31 @@ export function CallEndedView({ client }: { client: MatrixClient }) {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<Headline className={styles.headline}>
|
<Headline className={styles.headline}>
|
||||||
{displayName}, your call is now ended
|
{t("{{displayName}}, your call is now ended", { displayName })}
|
||||||
</Headline>
|
</Headline>
|
||||||
<div className={styles.callEndedContent}>
|
<div className={styles.callEndedContent}>
|
||||||
<Subtitle>
|
<Trans>
|
||||||
Why not finish by setting up a password to keep your account?
|
<Subtitle>
|
||||||
</Subtitle>
|
Why not finish by setting up a password to keep your account?
|
||||||
<Subtitle>
|
</Subtitle>
|
||||||
You'll be able to keep your name and set an avatar for use on
|
<Subtitle>
|
||||||
future calls
|
You'll be able to keep your name and set an avatar for use on
|
||||||
</Subtitle>
|
future calls
|
||||||
|
</Subtitle>
|
||||||
|
</Trans>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={styles.callEndedButton}
|
className={styles.callEndedButton}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="default"
|
variant="default"
|
||||||
to="/register"
|
to="/register"
|
||||||
>
|
>
|
||||||
Create account
|
{t("Create account")}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Body className={styles.footer}>
|
<Body className={styles.footer}>
|
||||||
<Link color="primary" to="/">
|
<Link color="primary" to="/">
|
||||||
Not now, return to home screen
|
{t("Not now, return to home screen")}
|
||||||
</Link>
|
</Link>
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, { useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
useRageshakeRequest,
|
useRageshakeRequest,
|
||||||
} from "../settings/submit-rageshake";
|
} from "../settings/submit-rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
inCall: boolean;
|
inCall: boolean;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -32,7 +34,9 @@ interface Props {
|
|||||||
// TODO: add all props for for <Modal>
|
// TODO: add all props for for <Modal>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
|
export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||||
const sendRageshakeRequest = useRageshakeRequest();
|
const sendRageshakeRequest = useRageshakeRequest();
|
||||||
|
|
||||||
@@ -67,15 +71,20 @@ export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
|
|||||||
}, [sent, onClose]);
|
}, [sent, onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="Submit Feedback" isDismissable onClose={onClose} {...rest}>
|
<Modal
|
||||||
|
title={t("Submit feedback")}
|
||||||
|
isDismissable
|
||||||
|
onClose={onClose}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<Body>Having trouble? Help us fix it.</Body>
|
<Body>{t("Having trouble? Help us fix it.")}</Body>
|
||||||
<form onSubmit={onSubmitFeedback}>
|
<form onSubmit={onSubmitFeedback}>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
label="Description (optional)"
|
label={t("Description (optional)")}
|
||||||
type="textarea"
|
type="textarea"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
@@ -83,19 +92,19 @@ export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
|
|||||||
<InputField
|
<InputField
|
||||||
id="sendLogs"
|
id="sendLogs"
|
||||||
name="sendLogs"
|
name="sendLogs"
|
||||||
label="Include Debug Logs"
|
label={t("Include debug logs")}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked
|
defaultChecked
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button type="submit" disabled={sending}>
|
<Button type="submit" disabled={sending}>
|
||||||
{sending ? "Submitting feedback..." : "Submit Feedback"}
|
{sending ? t("Submitting feedback…") : t("Submit feedback")}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
||||||
@@ -27,28 +28,33 @@ import { Menu } from "../Menu";
|
|||||||
import { TooltipTrigger } from "../Tooltip";
|
import { TooltipTrigger } from "../Tooltip";
|
||||||
|
|
||||||
export type Layout = "freedom" | "spotlight";
|
export type Layout = "freedom" | "spotlight";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
setLayout: (layout: Layout) => void;
|
setLayout: (layout: Layout) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GridLayoutMenu({ layout, setLayout }: Props) {
|
export function GridLayoutMenu({ layout, setLayout }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltip = useCallback(() => t("Change layout"), [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverMenuTrigger placement="bottom right">
|
<PopoverMenuTrigger placement="bottom right">
|
||||||
<TooltipTrigger tooltip={() => "Layout Type"}>
|
<TooltipTrigger tooltip={tooltip}>
|
||||||
<Button variant="icon">
|
<Button variant="icon">
|
||||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props: JSX.IntrinsicAttributes) => (
|
{(props: JSX.IntrinsicAttributes) => (
|
||||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
<Menu {...props} label={t("Grid layout menu")} onAction={setLayout}>
|
||||||
<Item key="freedom" textValue="Freedom">
|
<Item key="freedom" textValue={t("Freedom")}>
|
||||||
<FreedomIcon />
|
<FreedomIcon />
|
||||||
<span>Freedom</span>
|
<span>Freedom</span>
|
||||||
{layout === "freedom" && (
|
{layout === "freedom" && (
|
||||||
<CheckIcon className={menuStyles.checkIcon} />
|
<CheckIcon className={menuStyles.checkIcon} />
|
||||||
)}
|
)}
|
||||||
</Item>
|
</Item>
|
||||||
<Item key="spotlight" textValue="Spotlight">
|
<Item key="spotlight" textValue={t("Spotlight")}>
|
||||||
<SpotlightIcon />
|
<SpotlightIcon />
|
||||||
<span>Spotlight</span>
|
<span>Spotlight</span>
|
||||||
{layout === "spotlight" && (
|
{layout === "spotlight" && (
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import React, { ReactNode } from "react";
|
import React, { 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 { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
@@ -37,6 +38,7 @@ export function GroupCallLoader({
|
|||||||
children,
|
children,
|
||||||
createPtt,
|
createPtt,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { loading, error, groupCall } = useLoadGroupCall(
|
const { loading, error, groupCall } = useLoadGroupCall(
|
||||||
client,
|
client,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
@@ -44,12 +46,12 @@ export function GroupCallLoader({
|
|||||||
createPtt
|
createPtt
|
||||||
);
|
);
|
||||||
|
|
||||||
usePageTitle(groupCall ? groupCall.room.name : "Loading...");
|
usePageTitle(groupCall ? groupCall.room.name : t("Loading…"));
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<h1>Loading room...</h1>
|
<h1>{t("Loading room…")}</h1>
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import React, { useCallback, useEffect, 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 { 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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
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";
|
||||||
@@ -31,6 +33,7 @@ import { useRoomAvatar } from "./useRoomAvatar";
|
|||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
import { findDeviceByName, getDevices } from "../media-utils";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -79,8 +82,8 @@ export function GroupCallView({
|
|||||||
unencryptedEventsFromUsers,
|
unencryptedEventsFromUsers,
|
||||||
} = useGroupCall(groupCall);
|
} = useGroupCall(groupCall);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
const { setAudioInput, setVideoInput } = useMediaHandler();
|
const { setAudioInput, setVideoInput } = useMediaHandler();
|
||||||
|
|
||||||
const avatarUrl = useRoomAvatar(groupCall.room);
|
const avatarUrl = useRoomAvatar(groupCall.room);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -94,10 +97,45 @@ export function GroupCallView({
|
|||||||
if (widget && preload) {
|
if (widget && preload) {
|
||||||
// In preload mode, wait for a join action before entering
|
// In preload mode, wait for a join action before entering
|
||||||
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
// Get the available devices so we can match the selected device
|
||||||
|
// to its ID. This involves getting a media stream (see docs on
|
||||||
|
// the function) so we only do it once and re-use the result.
|
||||||
|
const devices = await getDevices();
|
||||||
|
|
||||||
const { audioInput, videoInput } = ev.detail
|
const { audioInput, videoInput } = ev.detail
|
||||||
.data as unknown as JoinCallData;
|
.data as unknown as JoinCallData;
|
||||||
if (audioInput !== null) setAudioInput(audioInput);
|
|
||||||
if (videoInput !== null) setVideoInput(videoInput);
|
if (audioInput !== null) {
|
||||||
|
const deviceId = await findDeviceByName(
|
||||||
|
audioInput,
|
||||||
|
"audioinput",
|
||||||
|
devices
|
||||||
|
);
|
||||||
|
if (!deviceId) {
|
||||||
|
logger.warn("Unknown audio input: " + audioInput);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`Found audio input ID ${deviceId} for name ${audioInput}`
|
||||||
|
);
|
||||||
|
setAudioInput(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoInput !== null) {
|
||||||
|
const deviceId = await findDeviceByName(
|
||||||
|
videoInput,
|
||||||
|
"videoinput",
|
||||||
|
devices
|
||||||
|
);
|
||||||
|
if (!deviceId) {
|
||||||
|
logger.warn("Unknown video input: " + videoInput);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`Found video input ID ${deviceId} for name ${videoInput}`
|
||||||
|
);
|
||||||
|
setVideoInput(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
groupCall.setMicrophoneMuted(audioInput === null),
|
groupCall.setMicrophoneMuted(audioInput === null),
|
||||||
groupCall.setLocalVideoMuted(videoInput === null),
|
groupCall.setLocalVideoMuted(videoInput === null),
|
||||||
@@ -203,7 +241,7 @@ export function GroupCallView({
|
|||||||
} else if (state === GroupCallState.Entering) {
|
} else if (state === GroupCallState.Entering) {
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<h1>Entering room...</h1>
|
<h1>{t("Entering room…")}</h1>
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
} else if (left) {
|
} else if (left) {
|
||||||
@@ -220,7 +258,7 @@ export function GroupCallView({
|
|||||||
} else if (isEmbedded) {
|
} else if (isEmbedded) {
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<h1>Loading room...</h1>
|
<h1>{t("Loading room…")}</h1>
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|||||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import styles from "./InCallView.module.css";
|
import styles from "./InCallView.module.css";
|
||||||
@@ -56,6 +58,8 @@ import { useFullscreen } from "../video-grid/useFullscreen";
|
|||||||
import { AudioContainer } from "../video-grid/AudioContainer";
|
import { AudioContainer } from "../video-grid/AudioContainer";
|
||||||
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
||||||
import { widget, ElementWidgetActions } from "../widget";
|
import { widget, ElementWidgetActions } from "../widget";
|
||||||
|
import { useJoinRule } from "./useJoinRule";
|
||||||
|
import { useUrlParams } from "../UrlParams";
|
||||||
|
|
||||||
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
|
||||||
@@ -112,7 +116,10 @@ export function InCallView({
|
|||||||
unencryptedEventsFromUsers,
|
unencryptedEventsFromUsers,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
|
const joinRule = useJoinRule(groupCall.room);
|
||||||
|
|
||||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||||
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
// Merge the refs so they can attach to the same element
|
// Merge the refs so they can attach to the same element
|
||||||
@@ -139,6 +146,8 @@ export function InCallView({
|
|||||||
|
|
||||||
useAudioOutputDevice(audioRef, audioOutput);
|
useAudioOutputDevice(audioRef, audioOutput);
|
||||||
|
|
||||||
|
const { hideScreensharing } = useUrlParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
widget?.api.transport.send(
|
widget?.api.transport.send(
|
||||||
layout === "freedom"
|
layout === "freedom"
|
||||||
@@ -215,14 +224,17 @@ export function InCallView({
|
|||||||
// window is too small to show everyone
|
// window is too small to show everyone
|
||||||
const maximisedParticipant = useMemo(
|
const maximisedParticipant = useMemo(
|
||||||
() =>
|
() =>
|
||||||
fullscreenParticipant ?? (bounds.height <= 500 && bounds.width <= 500)
|
fullscreenParticipant ??
|
||||||
|
(bounds.height <= 400 && bounds.width <= 400
|
||||||
? items.find((item) => item.focused) ??
|
? items.find((item) => item.focused) ??
|
||||||
items.find((item) => item.callFeed) ??
|
items.find((item) => item.callFeed) ??
|
||||||
null
|
null
|
||||||
: null,
|
: null),
|
||||||
[fullscreenParticipant, bounds, items]
|
[fullscreenParticipant, bounds, items]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const reducedControls = bounds.width <= 400;
|
||||||
|
|
||||||
const renderAvatar = useCallback(
|
const renderAvatar = useCallback(
|
||||||
(roomMember: RoomMember, width: number, height: number) => {
|
(roomMember: RoomMember, width: number, height: number) => {
|
||||||
const avatarUrl = roomMember.user?.avatarUrl;
|
const avatarUrl = roomMember.user?.avatarUrl;
|
||||||
@@ -245,7 +257,7 @@ export function InCallView({
|
|||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.centerMessage}>
|
<div className={styles.centerMessage}>
|
||||||
<p>Waiting for other participants...</p>
|
<p>{t("Waiting for other participants…")}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -325,18 +337,21 @@ export function InCallView({
|
|||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
|
||||||
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
|
||||||
{canScreenshare && !isSafari && !maximisedParticipant && (
|
{canScreenshare &&
|
||||||
<ScreenshareButton
|
!hideScreensharing &&
|
||||||
enabled={isScreensharing}
|
!isSafari &&
|
||||||
onPress={toggleScreensharing}
|
!reducedControls && (
|
||||||
/>
|
<ScreenshareButton
|
||||||
)}
|
enabled={isScreensharing}
|
||||||
{!maximisedParticipant && (
|
onPress={toggleScreensharing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!reducedControls && (
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
inCall
|
inCall
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
showInvite={true}
|
showInvite={joinRule === JoinRule.Public}
|
||||||
feedbackModalState={feedbackModalState}
|
feedbackModalState={feedbackModalState}
|
||||||
feedbackModalProps={feedbackModalProps}
|
feedbackModalProps={feedbackModalProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
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";
|
||||||
@@ -25,19 +26,23 @@ interface Props extends Omit<ModalProps, "title" | "children"> {
|
|||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => (
|
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
|
||||||
<Modal
|
const { t } = useTranslation();
|
||||||
title="Invite People"
|
|
||||||
isDismissable
|
return (
|
||||||
className={styles.inviteModal}
|
<Modal
|
||||||
{...rest}
|
title={t("Invite people")}
|
||||||
>
|
isDismissable
|
||||||
<ModalContent>
|
className={styles.inviteModal}
|
||||||
<p>Copy and share this meeting link</p>
|
{...rest}
|
||||||
<CopyButton
|
>
|
||||||
className={styles.copyButton}
|
<ModalContent>
|
||||||
value={getRoomUrl(roomIdOrAlias)}
|
<p>{t("Copy and share this call link")}</p>
|
||||||
/>
|
<CopyButton
|
||||||
</ModalContent>
|
className={styles.copyButton}
|
||||||
</Modal>
|
value={getRoomUrl(roomIdOrAlias)}
|
||||||
);
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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 { PressEvent } from "@react-types/shared";
|
import { PressEvent } from "@react-types/shared";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./LobbyView.module.css";
|
import styles from "./LobbyView.module.css";
|
||||||
import { Button, CopyButton } from "../button";
|
import { Button, CopyButton } from "../button";
|
||||||
@@ -66,6 +67,7 @@ export function LobbyView({
|
|||||||
isEmbedded,
|
isEmbedded,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { stream } = useCallFeed(localCallFeed);
|
const { stream } = useCallFeed(localCallFeed);
|
||||||
const {
|
const {
|
||||||
audioInput,
|
audioInput,
|
||||||
@@ -142,15 +144,15 @@ export function LobbyView({
|
|||||||
variant="secondaryCopy"
|
variant="secondaryCopy"
|
||||||
value={getRoomUrl(roomIdOrAlias)}
|
value={getRoomUrl(roomIdOrAlias)}
|
||||||
className={styles.copyButton}
|
className={styles.copyButton}
|
||||||
copiedMessage="Call link copied"
|
copiedMessage={t("Call link copied")}
|
||||||
>
|
>
|
||||||
Copy call link and join later
|
{t("Copy call link and join later")}
|
||||||
</CopyButton>
|
</CopyButton>
|
||||||
</div>
|
</div>
|
||||||
{!isEmbedded && (
|
{!isEmbedded && (
|
||||||
<Body className={styles.joinRoomFooter}>
|
<Body className={styles.joinRoomFooter}>
|
||||||
<Link color="primary" to="/">
|
<Link color="primary" to="/">
|
||||||
Take me Home
|
{t("Take me Home")}
|
||||||
</Link>
|
</Link>
|
||||||
</Body>
|
</Body>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import React, { useCallback } from "react";
|
|||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Menu } from "../Menu";
|
import { Menu } from "../Menu";
|
||||||
@@ -31,6 +32,7 @@ import { SettingsModal } from "../settings/SettingsModal";
|
|||||||
import { InviteModal } from "./InviteModal";
|
import { InviteModal } from "./InviteModal";
|
||||||
import { TooltipTrigger } from "../Tooltip";
|
import { TooltipTrigger } from "../Tooltip";
|
||||||
import { FeedbackModal } from "./FeedbackModal";
|
import { FeedbackModal } from "./FeedbackModal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
inCall: boolean;
|
inCall: boolean;
|
||||||
@@ -42,6 +44,7 @@ interface Props {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverflowMenu({
|
export function OverflowMenu({
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
inCall,
|
inCall,
|
||||||
@@ -50,6 +53,8 @@ export function OverflowMenu({
|
|||||||
feedbackModalState,
|
feedbackModalState,
|
||||||
feedbackModalProps,
|
feedbackModalProps,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
modalState: inviteModalState,
|
modalState: inviteModalState,
|
||||||
modalProps: inviteModalProps,
|
modalProps: inviteModalProps,
|
||||||
@@ -90,29 +95,31 @@ export function OverflowMenu({
|
|||||||
[feedbackModalState, inviteModalState, settingsModalState]
|
[feedbackModalState, inviteModalState, settingsModalState]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tooltip = useCallback(() => t("More"), [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PopoverMenuTrigger disableOnState>
|
<PopoverMenuTrigger disableOnState>
|
||||||
<TooltipTrigger tooltip={() => "More"} placement="top">
|
<TooltipTrigger tooltip={tooltip} placement="top">
|
||||||
<Button variant="toolbar">
|
<Button variant="toolbar">
|
||||||
<OverflowIcon />
|
<OverflowIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props: JSX.IntrinsicAttributes) => (
|
{(props: JSX.IntrinsicAttributes) => (
|
||||||
<Menu {...props} label="more menu" onAction={onAction}>
|
<Menu {...props} label={t("More menu")} onAction={onAction}>
|
||||||
{showInvite && (
|
{showInvite && (
|
||||||
<Item key="invite" textValue="Invite people">
|
<Item key="invite" textValue={t("Invite people")}>
|
||||||
<AddUserIcon />
|
<AddUserIcon />
|
||||||
<span>Invite people</span>
|
<span>{t("Invite people")}</span>
|
||||||
</Item>
|
</Item>
|
||||||
)}
|
)}
|
||||||
<Item key="settings" textValue="Settings">
|
<Item key="settings" textValue={t("Settings")}>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
<span>Settings</span>
|
<span>{t("Settings")}</span>
|
||||||
</Item>
|
</Item>
|
||||||
<Item key="feedback" textValue="Submit Feedback">
|
<Item key="feedback" textValue={t("Submit feedback")}>
|
||||||
<FeedbackIcon />
|
<FeedbackIcon />
|
||||||
<span>Submit Feedback</span>
|
<span>{t("Submit feedback")}</span>
|
||||||
</Item>
|
</Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ limitations under the License.
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
|
import i18n from "i18next";
|
||||||
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 { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useDelayedState } from "../useDelayedState";
|
import { useDelayedState } from "../useDelayedState";
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
@@ -50,40 +52,45 @@ function getPromptText(
|
|||||||
talkOverEnabled: boolean,
|
talkOverEnabled: boolean,
|
||||||
activeSpeakerUserId: string,
|
activeSpeakerUserId: string,
|
||||||
activeSpeakerDisplayName: string,
|
activeSpeakerDisplayName: string,
|
||||||
connected: boolean
|
connected: boolean,
|
||||||
|
t: typeof i18n.t
|
||||||
): string {
|
): string {
|
||||||
if (!connected) return "Connection lost";
|
if (!connected) return t("Connection lost");
|
||||||
|
|
||||||
const isTouchScreen = Boolean(window.ontouchstart !== undefined);
|
const isTouchScreen = Boolean(window.ontouchstart !== undefined);
|
||||||
|
|
||||||
if (networkWaiting) {
|
if (networkWaiting) {
|
||||||
return "Waiting for network";
|
return t("Waiting for network");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showTalkOverError) {
|
if (showTalkOverError) {
|
||||||
return "You can't talk at the same time";
|
return t("You can't talk at the same time");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pttButtonHeld && activeSpeakerIsLocalUser) {
|
if (pttButtonHeld && activeSpeakerIsLocalUser) {
|
||||||
if (isTouchScreen) {
|
if (isTouchScreen) {
|
||||||
return "Release to stop";
|
return t("Release to stop");
|
||||||
} else {
|
} else {
|
||||||
return "Release spacebar key to stop";
|
return t("Release spacebar key to stop");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (talkOverEnabled && activeSpeakerUserId && !activeSpeakerIsLocalUser) {
|
if (talkOverEnabled && activeSpeakerUserId && !activeSpeakerIsLocalUser) {
|
||||||
if (isTouchScreen) {
|
if (isTouchScreen) {
|
||||||
return `Press and hold to talk over ${activeSpeakerDisplayName}`;
|
return t("Press and hold to talk over {{name}}", {
|
||||||
|
name: activeSpeakerDisplayName,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return `Press and hold spacebar to talk over ${activeSpeakerDisplayName}`;
|
return t("Press and hold spacebar to talk over {{name}}", {
|
||||||
|
name: activeSpeakerDisplayName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTouchScreen) {
|
if (isTouchScreen) {
|
||||||
return "Press and hold to talk";
|
return t("Press and hold to talk");
|
||||||
} else {
|
} else {
|
||||||
return "Press and hold spacebar to talk";
|
return t("Press and hold spacebar to talk");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +119,7 @@ export const PTTCallView: React.FC<Props> = ({
|
|||||||
isEmbedded,
|
isEmbedded,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||||
useModalTriggerState();
|
useModalTriggerState();
|
||||||
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
|
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
|
||||||
@@ -195,9 +203,11 @@ export const PTTCallView: React.FC<Props> = ({
|
|||||||
{showControls && (
|
{showControls && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.participants}>
|
<div className={styles.participants}>
|
||||||
<p>{`${participants.length} ${
|
<p>
|
||||||
participants.length > 1 ? "people" : "person"
|
{t("{{count}} people connected", {
|
||||||
} connected`}</p>
|
count: participants.length,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
<Facepile
|
<Facepile
|
||||||
size={facepileSize}
|
size={facepileSize}
|
||||||
max={8}
|
max={8}
|
||||||
@@ -230,8 +240,10 @@ export const PTTCallView: React.FC<Props> = ({
|
|||||||
<AudioIcon className={styles.speakerIcon} />
|
<AudioIcon className={styles.speakerIcon} />
|
||||||
)}
|
)}
|
||||||
{activeSpeakerIsLocalUser
|
{activeSpeakerIsLocalUser
|
||||||
? "Talking..."
|
? t("Talking…")
|
||||||
: `${activeSpeakerDisplayName} is talking...`}
|
: t("{{name}} is talking…", {
|
||||||
|
name: activeSpeakerDisplayName,
|
||||||
|
})}
|
||||||
</h2>
|
</h2>
|
||||||
<Timer value={activeSpeakerUserId} />
|
<Timer value={activeSpeakerUserId} />
|
||||||
</div>
|
</div>
|
||||||
@@ -263,7 +275,8 @@ export const PTTCallView: React.FC<Props> = ({
|
|||||||
talkOverEnabled,
|
talkOverEnabled,
|
||||||
activeSpeakerUserId,
|
activeSpeakerUserId,
|
||||||
activeSpeakerDisplayName,
|
activeSpeakerDisplayName,
|
||||||
connected
|
connected,
|
||||||
|
t
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -278,7 +291,7 @@ export const PTTCallView: React.FC<Props> = ({
|
|||||||
<Toggle
|
<Toggle
|
||||||
isSelected={talkOverEnabled}
|
isSelected={talkOverEnabled}
|
||||||
onChange={setTalkOverEnabled}
|
onChange={setTalkOverEnabled}
|
||||||
label="Talk over speaker"
|
label={t("Talk over speaker")}
|
||||||
id="talkOverEnabled"
|
id="talkOverEnabled"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useEffect } from "react";
|
import React, { FC, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Modal, ModalContent, ModalProps } from "../Modal";
|
import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -33,6 +34,7 @@ export const RageshakeRequestModal: FC<Props> = ({
|
|||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,11 +44,12 @@ export const RageshakeRequestModal: FC<Props> = ({
|
|||||||
}, [sent, rest]);
|
}, [sent, rest]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="Debug Log Request" isDismissable {...rest}>
|
<Modal title={t("Debug log request")} isDismissable {...rest}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<Body>
|
<Body>
|
||||||
Another user on this call is having an issue. In order to better
|
{t(
|
||||||
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."
|
||||||
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button
|
<Button
|
||||||
@@ -59,12 +62,12 @@ export const RageshakeRequestModal: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
>
|
>
|
||||||
{sending ? "Sending debug log..." : "Send debug log"}
|
{sending ? t("Sending debug logs…") : t("Send debug logs")}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./RoomAuthView.module.css";
|
import styles from "./RoomAuthView.module.css";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -50,6 +51,7 @@ export function RoomAuthView() {
|
|||||||
[registerPasswordlessUser]
|
[registerPasswordlessUser]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -64,42 +66,46 @@ export function RoomAuthView() {
|
|||||||
</Header>
|
</Header>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<Headline className={styles.headline}>Join Call</Headline>
|
<Headline className={styles.headline}>{t("Join call")}</Headline>
|
||||||
<Form className={styles.form} onSubmit={onSubmit}>
|
<Form className={styles.form} onSubmit={onSubmit}>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="displayName"
|
id="displayName"
|
||||||
name="displayName"
|
name="displayName"
|
||||||
label="Display Name"
|
label={t("Display name")}
|
||||||
placeholder="Display Name"
|
placeholder={t("Display name")}
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Caption>
|
<Caption>
|
||||||
By clicking "Join call now", you agree to our{" "}
|
<Trans>
|
||||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
By clicking "Join call now", you agree to our{" "}
|
||||||
|
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||||
|
</Trans>
|
||||||
</Caption>
|
</Caption>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" size="lg" disabled={loading}>
|
<Button type="submit" size="lg" disabled={loading}>
|
||||||
{loading ? "Loading..." : "Join call now"}
|
{loading ? t("Loading…") : t("Join call now")}
|
||||||
</Button>
|
</Button>
|
||||||
<div id={recaptchaId} />
|
<div id={recaptchaId} />
|
||||||
</Form>
|
</Form>
|
||||||
</main>
|
</main>
|
||||||
<Body className={styles.footer}>
|
<Body className={styles.footer}>
|
||||||
{"Not registered yet? "}
|
<Trans>
|
||||||
<Link
|
Not registered yet?{" "}
|
||||||
color="primary"
|
<Link
|
||||||
to={{ pathname: "/login", state: { from: location } }}
|
color="primary"
|
||||||
>
|
to={{ pathname: "/login", state: { from: location } }}
|
||||||
Create an account
|
>
|
||||||
</Link>
|
Create an account
|
||||||
|
</Link>
|
||||||
|
</Trans>
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useEffect, useState, useCallback } from "react";
|
import React, { FC, useEffect, useState, useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
@@ -22,11 +23,13 @@ import { ErrorView, LoadingView } from "../FullScreenView";
|
|||||||
import { RoomAuthView } from "./RoomAuthView";
|
import { RoomAuthView } from "./RoomAuthView";
|
||||||
import { GroupCallLoader } from "./GroupCallLoader";
|
import { GroupCallLoader } from "./GroupCallLoader";
|
||||||
import { GroupCallView } from "./GroupCallView";
|
import { GroupCallView } from "./GroupCallView";
|
||||||
import { useRoomParams } from "./useRoomParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
import { MediaHandlerProvider } from "../settings/useMediaHandler";
|
import { MediaHandlerProvider } from "../settings/useMediaHandler";
|
||||||
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
||||||
|
import { translatedError } from "../TranslatedError";
|
||||||
|
|
||||||
export const RoomPage: FC = () => {
|
export const RoomPage: FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||||
useClient();
|
useClient();
|
||||||
|
|
||||||
@@ -39,9 +42,9 @@ export const RoomPage: FC = () => {
|
|||||||
hideHeader,
|
hideHeader,
|
||||||
isPtt,
|
isPtt,
|
||||||
displayName,
|
displayName,
|
||||||
} = useRoomParams();
|
} = useUrlParams();
|
||||||
const roomIdOrAlias = roomId ?? roomAlias;
|
const roomIdOrAlias = roomId ?? roomAlias;
|
||||||
if (!roomIdOrAlias) throw new Error("No room specified");
|
if (!roomIdOrAlias) throw translatedError("No room specified", t);
|
||||||
|
|
||||||
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
|
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
|
||||||
const [isRegistering, setIsRegistering] = useState(false);
|
const [isRegistering, setIsRegistering] = useState(false);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import useMeasure from "react-use-measure";
|
|||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { 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 { MicButton, VideoButton } from "../button";
|
import { MicButton, VideoButton } from "../button";
|
||||||
import { useMediaStream } from "../video-grid/useMediaStream";
|
import { useMediaStream } from "../video-grid/useMediaStream";
|
||||||
@@ -40,6 +41,7 @@ interface Props {
|
|||||||
audioOutput: string;
|
audioOutput: string;
|
||||||
stream: MediaStream;
|
stream: MediaStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoPreview({
|
export function VideoPreview({
|
||||||
client,
|
client,
|
||||||
state,
|
state,
|
||||||
@@ -51,6 +53,7 @@ export function VideoPreview({
|
|||||||
audioOutput,
|
audioOutput,
|
||||||
stream,
|
stream,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const videoRef = useMediaStream(stream, audioOutput, true);
|
const videoRef = useMediaStream(stream, audioOutput, true);
|
||||||
const { displayName, avatarUrl } = useProfile(client);
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
@@ -64,12 +67,12 @@ export function VideoPreview({
|
|||||||
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
<video ref={videoRef} muted playsInline disablePictureInPicture />
|
||||||
{state === GroupCallState.LocalCallFeedUninitialized && (
|
{state === GroupCallState.LocalCallFeedUninitialized && (
|
||||||
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
|
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
|
||||||
Camera/microphone permissions needed to join the call.
|
{t("Camera/microphone permissions needed to join the call.")}
|
||||||
</Body>
|
</Body>
|
||||||
)}
|
)}
|
||||||
{state === GroupCallState.InitializingLocalCallFeed && (
|
{state === GroupCallState.InitializingLocalCallFeed && (
|
||||||
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
|
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
|
||||||
Accept camera/microphone permissions to join the call.
|
{t("Accept camera/microphone permissions to join the call.")}
|
||||||
</Body>
|
</Body>
|
||||||
)}
|
)}
|
||||||
{state === GroupCallState.LocalCallFeedInitialized && (
|
{state === GroupCallState.LocalCallFeedInitialized && (
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import {
|
|||||||
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
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 { usePageUnload } from "./usePageUnload";
|
import { usePageUnload } from "./usePageUnload";
|
||||||
|
import { TranslatedError, translatedError } from "../TranslatedError";
|
||||||
|
|
||||||
export interface UseGroupCallReturnType {
|
export interface UseGroupCallReturnType {
|
||||||
state: GroupCallState;
|
state: GroupCallState;
|
||||||
@@ -37,7 +39,7 @@ export interface UseGroupCallReturnType {
|
|||||||
userMediaFeeds: CallFeed[];
|
userMediaFeeds: CallFeed[];
|
||||||
microphoneMuted: boolean;
|
microphoneMuted: boolean;
|
||||||
localVideoMuted: boolean;
|
localVideoMuted: boolean;
|
||||||
error: Error;
|
error: TranslatedError | null;
|
||||||
initLocalCallFeed: () => void;
|
initLocalCallFeed: () => void;
|
||||||
enter: () => void;
|
enter: () => void;
|
||||||
leave: () => void;
|
leave: () => void;
|
||||||
@@ -60,7 +62,7 @@ interface State {
|
|||||||
localCallFeed: CallFeed;
|
localCallFeed: CallFeed;
|
||||||
activeSpeaker: string;
|
activeSpeaker: string;
|
||||||
userMediaFeeds: CallFeed[];
|
userMediaFeeds: CallFeed[];
|
||||||
error: Error;
|
error: TranslatedError | null;
|
||||||
microphoneMuted: boolean;
|
microphoneMuted: boolean;
|
||||||
localVideoMuted: boolean;
|
localVideoMuted: boolean;
|
||||||
screenshareFeeds: CallFeed[];
|
screenshareFeeds: CallFeed[];
|
||||||
@@ -309,15 +311,18 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||||||
});
|
});
|
||||||
}, [groupCall]);
|
}, [groupCall]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.RTCPeerConnection === undefined) {
|
if (window.RTCPeerConnection === undefined) {
|
||||||
const error = new Error(
|
const error = translatedError(
|
||||||
"WebRTC is not supported or is being blocked in this browser."
|
"WebRTC is not supported or is being blocked in this browser.",
|
||||||
|
t
|
||||||
);
|
);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
updateState({ error });
|
updateState({ error });
|
||||||
}
|
}
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
|
|||||||
26
src/room/useJoinRule.ts
Normal file
26
src/room/useJoinRule.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
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 type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { useRoomState } from "./useRoomState";
|
||||||
|
|
||||||
|
export const useJoinRule = (room: Room) =>
|
||||||
|
useRoomState(
|
||||||
|
room,
|
||||||
|
useCallback((state) => state.getJoinRule(), [])
|
||||||
|
);
|
||||||
@@ -24,10 +24,12 @@ import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEv
|
|||||||
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 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 { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||||
|
import { translatedError } from "../TranslatedError";
|
||||||
|
|
||||||
export interface GroupCallLoadState {
|
export interface GroupCallLoadState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -41,6 +43,7 @@ export const useLoadGroupCall = (
|
|||||||
viaServers: string[],
|
viaServers: string[],
|
||||||
createPtt: boolean
|
createPtt: boolean
|
||||||
): GroupCallLoadState => {
|
): GroupCallLoadState => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [state, setState] = useState<GroupCallLoadState>({ loading: true });
|
const [state, setState] = useState<GroupCallLoadState>({ loading: true });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -122,7 +125,7 @@ export const useLoadGroupCall = (
|
|||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
client.off(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming);
|
client.off(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming);
|
||||||
reject(new Error("Fetching group call timed out."));
|
reject(translatedError("Fetching group call timed out.", t));
|
||||||
}, 30000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -153,7 +156,7 @@ export const useLoadGroupCall = (
|
|||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
setState((prevState) => ({ ...prevState, loading: false, error }))
|
setState((prevState) => ({ ...prevState, loading: false, error }))
|
||||||
);
|
);
|
||||||
}, [client, roomIdOrAlias, viaServers, createPtt]);
|
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function isIOS() {
|
|||||||
|
|
||||||
export function usePageUnload(callback: () => void) {
|
export function usePageUnload(callback: () => void) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let pageVisibilityTimeout: number;
|
let pageVisibilityTimeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
function onBeforeUnload(event: PageTransitionEvent) {
|
function onBeforeUnload(event: PageTransitionEvent) {
|
||||||
if (event.type === "visibilitychange") {
|
if (event.type === "visibilitychange") {
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import { useState, useEffect } from "react";
|
/*
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
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 { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
|
||||||
|
|
||||||
export const useRoomAvatar = (room: Room) => {
|
import { useRoomState } from "./useRoomState";
|
||||||
const [avatarUrl, setAvatarUrl] = useState(room.getMxcAvatarUrl());
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const useRoomAvatar = (room: Room) =>
|
||||||
const update = (ev: MatrixEvent) => {
|
useRoomState(
|
||||||
if (ev.getType() === EventType.RoomAvatar) {
|
room,
|
||||||
setAvatarUrl(room.getMxcAvatarUrl());
|
useCallback(() => room.getMxcAvatarUrl(), [room])
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
room.currentState.on(RoomStateEvent.Events, update);
|
|
||||||
return () => {
|
|
||||||
room.currentState.off(RoomStateEvent.Events, update);
|
|
||||||
};
|
|
||||||
}, [room]);
|
|
||||||
|
|
||||||
return avatarUrl;
|
|
||||||
};
|
|
||||||
|
|||||||
39
src/room/useRoomState.ts
Normal file
39
src/room/useRoomState.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
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 { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { useTypedEventEmitter } from "../useEvents";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A React hook for values computed from room state.
|
||||||
|
* @param room The room.
|
||||||
|
* @param f A mapping from the current room state to the computed value.
|
||||||
|
* @returns The computed value.
|
||||||
|
*/
|
||||||
|
export const useRoomState = <T>(room: Room, f: (state: RoomState) => T): T => {
|
||||||
|
const [numUpdates, setNumUpdates] = useState(0);
|
||||||
|
useTypedEventEmitter(
|
||||||
|
room,
|
||||||
|
RoomStateEvent.Update,
|
||||||
|
useCallback(() => setNumUpdates((n) => n + 1), [setNumUpdates])
|
||||||
|
);
|
||||||
|
// We want any change to the update counter to trigger an update here
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
return useMemo(() => f(room.currentState), [room, f, numUpdates]);
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import styles from "./SettingsModal.module.css";
|
import styles from "./SettingsModal.module.css";
|
||||||
@@ -37,6 +38,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsModal = (props: Props) => {
|
export const SettingsModal = (props: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
audioInput,
|
audioInput,
|
||||||
audioInputs,
|
audioInputs,
|
||||||
@@ -56,7 +58,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Settings"
|
title={t("Settings")}
|
||||||
isDismissable
|
isDismissable
|
||||||
mobileFullScreen
|
mobileFullScreen
|
||||||
className={styles.settingsModal}
|
className={styles.settingsModal}
|
||||||
@@ -67,12 +69,12 @@ export const SettingsModal = (props: Props) => {
|
|||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<AudioIcon width={16} height={16} />
|
<AudioIcon width={16} height={16} />
|
||||||
<span>Audio</span>
|
<span>{t("Audio")}</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label="Microphone"
|
label={t("Microphone")}
|
||||||
selectedKey={audioInput}
|
selectedKey={audioInput}
|
||||||
onSelectionChange={setAudioInput}
|
onSelectionChange={setAudioInput}
|
||||||
>
|
>
|
||||||
@@ -80,13 +82,13 @@ export const SettingsModal = (props: Props) => {
|
|||||||
<Item key={deviceId}>
|
<Item key={deviceId}>
|
||||||
{!!label && label.trim().length > 0
|
{!!label && label.trim().length > 0
|
||||||
? label
|
? label
|
||||||
: `Microphone ${index + 1}`}
|
: t("Microphone {{n}}", { n: index + 1 })}
|
||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</SelectInput>
|
</SelectInput>
|
||||||
{audioOutputs.length > 0 && (
|
{audioOutputs.length > 0 && (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label="Speaker"
|
label={t("Speaker")}
|
||||||
selectedKey={audioOutput}
|
selectedKey={audioOutput}
|
||||||
onSelectionChange={setAudioOutput}
|
onSelectionChange={setAudioOutput}
|
||||||
>
|
>
|
||||||
@@ -94,7 +96,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
<Item key={deviceId}>
|
<Item key={deviceId}>
|
||||||
{!!label && label.trim().length > 0
|
{!!label && label.trim().length > 0
|
||||||
? label
|
? label
|
||||||
: `Speaker ${index + 1}`}
|
: t("Speaker {{n}}", { n: index + 1 })}
|
||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</SelectInput>
|
</SelectInput>
|
||||||
@@ -102,10 +104,12 @@ export const SettingsModal = (props: Props) => {
|
|||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="spatialAudio"
|
id="spatialAudio"
|
||||||
label="Spatial audio"
|
label={t("Spatial audio")}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={spatialAudio}
|
checked={spatialAudio}
|
||||||
description="This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)"
|
description={t(
|
||||||
|
"This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)"
|
||||||
|
)}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
setSpatialAudio(event.target.checked)
|
setSpatialAudio(event.target.checked)
|
||||||
}
|
}
|
||||||
@@ -116,12 +120,12 @@ export const SettingsModal = (props: Props) => {
|
|||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<VideoIcon width={16} height={16} />
|
<VideoIcon width={16} height={16} />
|
||||||
<span>Video</span>
|
<span>{t("Video")}</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label="Camera"
|
label={t("Camera")}
|
||||||
selectedKey={videoInput}
|
selectedKey={videoInput}
|
||||||
onSelectionChange={setVideoInput}
|
onSelectionChange={setVideoInput}
|
||||||
>
|
>
|
||||||
@@ -129,7 +133,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
<Item key={deviceId}>
|
<Item key={deviceId}>
|
||||||
{!!label && label.trim().length > 0
|
{!!label && label.trim().length > 0
|
||||||
? label
|
? label
|
||||||
: `Camera ${index + 1}`}
|
: t("Camera {{n}}", { n: index + 1 })}
|
||||||
</Item>
|
</Item>
|
||||||
))}
|
))}
|
||||||
</SelectInput>
|
</SelectInput>
|
||||||
@@ -138,20 +142,22 @@ export const SettingsModal = (props: Props) => {
|
|||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<DeveloperIcon width={16} height={16} />
|
<DeveloperIcon width={16} height={16} />
|
||||||
<span>Developer</span>
|
<span>{t("Developer")}</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Body className={styles.fieldRowText}>
|
<Body className={styles.fieldRowText}>
|
||||||
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
|
{t("Version: {{version}}", {
|
||||||
|
version: import.meta.env.VITE_APP_VERSION || "dev",
|
||||||
|
})}
|
||||||
</Body>
|
</Body>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="showInspector"
|
id="showInspector"
|
||||||
name="inspector"
|
name="inspector"
|
||||||
label="Show Call Inspector"
|
label={t("Show call inspector")}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showInspector}
|
checked={showInspector}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@@ -160,7 +166,9 @@ export const SettingsModal = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button onPress={downloadDebugLog}>Download Debug Logs</Button>
|
<Button onPress={downloadDebugLog}>
|
||||||
|
{t("Download debug logs")}
|
||||||
|
</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ limitations under the License.
|
|||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Listener,
|
||||||
|
ListenerMap,
|
||||||
|
TypedEventEmitter,
|
||||||
|
} from "matrix-js-sdk/src/models/typed-event-emitter";
|
||||||
|
|
||||||
// Shortcut for registering a listener on an EventTarget
|
// Shortcut for registering a listener on an EventTarget
|
||||||
export const useEventTarget = <T extends Event>(
|
export const useEventTarget = <T extends Event>(
|
||||||
target: EventTarget,
|
target: EventTarget,
|
||||||
@@ -31,4 +37,20 @@ export const useEventTarget = <T extends Event>(
|
|||||||
}, [target, eventType, listener, options]);
|
}, [target, eventType, listener, options]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Have a similar hook for EventEmitters
|
// Shortcut for registering a listener on a TypedEventEmitter
|
||||||
|
export const useTypedEventEmitter = <
|
||||||
|
Events extends string,
|
||||||
|
Arguments extends ListenerMap<Events>,
|
||||||
|
T extends Events
|
||||||
|
>(
|
||||||
|
emitter: TypedEventEmitter<Events, Arguments>,
|
||||||
|
eventType: T,
|
||||||
|
listener: Listener<Events, Arguments, T>
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
emitter.on(eventType, listener);
|
||||||
|
return () => {
|
||||||
|
emitter.off(eventType, listener);
|
||||||
|
};
|
||||||
|
}, [emitter, eventType, listener]);
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { FC, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { Participant } from "../room/InCallView";
|
import { Participant } from "../room/InCallView";
|
||||||
import { useCallFeed } from "./useCallFeed";
|
import { useCallFeed } from "./useCallFeed";
|
||||||
@@ -29,19 +29,22 @@ interface AudioForParticipantProps {
|
|||||||
audioDestination: AudioNode;
|
audioDestination: AudioNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AudioForParticipant({
|
export const AudioForParticipant: FC<AudioForParticipantProps> = ({
|
||||||
item,
|
item,
|
||||||
audioContext,
|
audioContext,
|
||||||
audioDestination,
|
audioDestination,
|
||||||
}: AudioForParticipantProps): JSX.Element {
|
}) => {
|
||||||
const { stream, localVolume, audioMuted } = useCallFeed(item.callFeed);
|
const { stream, localVolume } = useCallFeed(item.callFeed);
|
||||||
const [audioTrackCount] = useMediaStreamTrackCount(stream);
|
const [audioTrackCount] = useMediaStreamTrackCount(stream);
|
||||||
|
|
||||||
const gainNodeRef = useRef<GainNode>();
|
const gainNodeRef = useRef<GainNode>();
|
||||||
const sourceRef = useRef<MediaStreamAudioSourceNode>();
|
const sourceRef = useRef<MediaStreamAudioSourceNode>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!item.isLocal && audioContext && !audioMuted && audioTrackCount > 0) {
|
// We don't compare the audioMuted flag of useCallFeed here, since unmuting
|
||||||
|
// depends on to-device messages which may lag behind the audio actually
|
||||||
|
// starting to flow over the network
|
||||||
|
if (!item.isLocal && audioContext && audioTrackCount > 0) {
|
||||||
if (!gainNodeRef.current) {
|
if (!gainNodeRef.current) {
|
||||||
gainNodeRef.current = new GainNode(audioContext, {
|
gainNodeRef.current = new GainNode(audioContext, {
|
||||||
gain: localVolume,
|
gain: localVolume,
|
||||||
@@ -68,12 +71,11 @@ export function AudioForParticipant({
|
|||||||
audioDestination,
|
audioDestination,
|
||||||
stream,
|
stream,
|
||||||
localVolume,
|
localVolume,
|
||||||
audioMuted,
|
|
||||||
audioTrackCount,
|
audioTrackCount,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface AudioContainerProps {
|
interface AudioContainerProps {
|
||||||
items: Participant[];
|
items: Participant[];
|
||||||
@@ -81,10 +83,7 @@ interface AudioContainerProps {
|
|||||||
audioDestination: AudioNode;
|
audioDestination: AudioNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AudioContainer({
|
export const AudioContainer: FC<AudioContainerProps> = ({ items, ...rest }) => {
|
||||||
items,
|
|
||||||
...rest
|
|
||||||
}: AudioContainerProps): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{items
|
{items
|
||||||
@@ -94,4 +93,4 @@ export function AudioContainer({
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ const getPipGap = (gridAspectRatio: number): number =>
|
|||||||
|
|
||||||
function getTilePositions(
|
function getTilePositions(
|
||||||
tileCount: number,
|
tileCount: number,
|
||||||
presenterTileCount: number,
|
focusedTileCount: number,
|
||||||
|
hasPresenter: boolean,
|
||||||
gridWidth: number,
|
gridWidth: number,
|
||||||
gridHeight: number,
|
gridHeight: number,
|
||||||
pipXRatio: number,
|
pipXRatio: number,
|
||||||
@@ -119,7 +120,7 @@ function getTilePositions(
|
|||||||
layout: Layout
|
layout: Layout
|
||||||
): TilePosition[] {
|
): TilePosition[] {
|
||||||
if (layout === "freedom") {
|
if (layout === "freedom") {
|
||||||
if (tileCount === 2 && presenterTileCount === 0) {
|
if (tileCount === 2 && !hasPresenter) {
|
||||||
return getOneOnOneLayoutTilePositions(
|
return getOneOnOneLayoutTilePositions(
|
||||||
gridWidth,
|
gridWidth,
|
||||||
gridHeight,
|
gridHeight,
|
||||||
@@ -130,7 +131,7 @@ function getTilePositions(
|
|||||||
|
|
||||||
return getFreedomLayoutTilePositions(
|
return getFreedomLayoutTilePositions(
|
||||||
tileCount,
|
tileCount,
|
||||||
presenterTileCount,
|
focusedTileCount,
|
||||||
gridWidth,
|
gridWidth,
|
||||||
gridHeight
|
gridHeight
|
||||||
);
|
);
|
||||||
@@ -247,7 +248,7 @@ function getSpotlightLayoutTilePositions(
|
|||||||
|
|
||||||
function getFreedomLayoutTilePositions(
|
function getFreedomLayoutTilePositions(
|
||||||
tileCount: number,
|
tileCount: number,
|
||||||
presenterTileCount: number,
|
focusedTileCount: number,
|
||||||
gridWidth: number,
|
gridWidth: number,
|
||||||
gridHeight: number
|
gridHeight: number
|
||||||
): TilePosition[] {
|
): TilePosition[] {
|
||||||
@@ -261,7 +262,7 @@ function getFreedomLayoutTilePositions(
|
|||||||
|
|
||||||
const { layoutDirection, itemGridRatio } = getGridLayout(
|
const { layoutDirection, itemGridRatio } = getGridLayout(
|
||||||
tileCount,
|
tileCount,
|
||||||
presenterTileCount,
|
focusedTileCount,
|
||||||
gridWidth,
|
gridWidth,
|
||||||
gridHeight
|
gridHeight
|
||||||
);
|
);
|
||||||
@@ -277,7 +278,7 @@ function getFreedomLayoutTilePositions(
|
|||||||
itemGridHeight = gridHeight;
|
itemGridHeight = gridHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemTileCount = tileCount - presenterTileCount;
|
const itemTileCount = tileCount - focusedTileCount;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
columnCount: itemColumnCount,
|
columnCount: itemColumnCount,
|
||||||
@@ -295,65 +296,55 @@ function getFreedomLayoutTilePositions(
|
|||||||
);
|
);
|
||||||
const itemGridBounds = getSubGridBoundingBox(itemGridPositions);
|
const itemGridBounds = getSubGridBoundingBox(itemGridPositions);
|
||||||
|
|
||||||
let presenterGridWidth;
|
let focusedGridWidth: number;
|
||||||
let presenterGridHeight;
|
let focusedGridHeight: number;
|
||||||
|
|
||||||
if (presenterTileCount === 0) {
|
if (focusedTileCount === 0) {
|
||||||
presenterGridWidth = 0;
|
focusedGridWidth = 0;
|
||||||
presenterGridHeight = 0;
|
focusedGridHeight = 0;
|
||||||
} else if (layoutDirection === "vertical") {
|
} else if (layoutDirection === "vertical") {
|
||||||
presenterGridWidth = gridWidth;
|
focusedGridWidth = gridWidth;
|
||||||
presenterGridHeight =
|
focusedGridHeight =
|
||||||
gridHeight - (itemGridBounds.height + (itemTileCount ? GAP * 2 : 0));
|
gridHeight - (itemGridBounds.height + (itemTileCount ? GAP * 2 : 0));
|
||||||
} else {
|
} else {
|
||||||
presenterGridWidth =
|
focusedGridWidth =
|
||||||
gridWidth - (itemGridBounds.width + (itemTileCount ? GAP * 2 : 0));
|
gridWidth - (itemGridBounds.width + (itemTileCount ? GAP * 2 : 0));
|
||||||
presenterGridHeight = gridHeight;
|
focusedGridHeight = gridHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
columnCount: presenterColumnCount,
|
columnCount: focusedColumnCount,
|
||||||
rowCount: presenterRowCount,
|
rowCount: focusedRowCount,
|
||||||
tileAspectRatio: presenterTileAspectRatio,
|
tileAspectRatio: focusedTileAspectRatio,
|
||||||
} = getSubGridLayout(
|
} = getSubGridLayout(focusedTileCount, focusedGridWidth, focusedGridHeight);
|
||||||
presenterTileCount,
|
|
||||||
presenterGridWidth,
|
const focusedGridPositions = getSubGridPositions(
|
||||||
presenterGridHeight
|
focusedTileCount,
|
||||||
|
focusedColumnCount,
|
||||||
|
focusedRowCount,
|
||||||
|
focusedTileAspectRatio,
|
||||||
|
focusedGridWidth,
|
||||||
|
focusedGridHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
const presenterGridPositions = getSubGridPositions(
|
const tilePositions = [...focusedGridPositions, ...itemGridPositions];
|
||||||
presenterTileCount,
|
|
||||||
presenterColumnCount,
|
|
||||||
presenterRowCount,
|
|
||||||
presenterTileAspectRatio,
|
|
||||||
presenterGridWidth,
|
|
||||||
presenterGridHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
const tilePositions = [...presenterGridPositions, ...itemGridPositions];
|
centerTiles(focusedGridPositions, focusedGridWidth, focusedGridHeight, 0, 0);
|
||||||
|
|
||||||
centerTiles(
|
|
||||||
presenterGridPositions,
|
|
||||||
presenterGridWidth,
|
|
||||||
presenterGridHeight,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (layoutDirection === "vertical") {
|
if (layoutDirection === "vertical") {
|
||||||
centerTiles(
|
centerTiles(
|
||||||
itemGridPositions,
|
itemGridPositions,
|
||||||
gridWidth,
|
gridWidth,
|
||||||
gridHeight - presenterGridHeight,
|
gridHeight - focusedGridHeight,
|
||||||
0,
|
0,
|
||||||
presenterGridHeight
|
focusedGridHeight
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
centerTiles(
|
centerTiles(
|
||||||
itemGridPositions,
|
itemGridPositions,
|
||||||
gridWidth - presenterGridWidth,
|
gridWidth - focusedGridWidth,
|
||||||
gridHeight,
|
gridHeight,
|
||||||
presenterGridWidth,
|
focusedGridWidth,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -418,14 +409,14 @@ function isMobileBreakpoint(gridWidth: number, gridHeight: number): boolean {
|
|||||||
|
|
||||||
function getGridLayout(
|
function getGridLayout(
|
||||||
tileCount: number,
|
tileCount: number,
|
||||||
presenterTileCount: number,
|
focusedTileCount: number,
|
||||||
gridWidth: number,
|
gridWidth: number,
|
||||||
gridHeight: number
|
gridHeight: number
|
||||||
): { itemGridRatio: number; layoutDirection: LayoutDirection } {
|
): { itemGridRatio: number; layoutDirection: LayoutDirection } {
|
||||||
let layoutDirection: LayoutDirection = "horizontal";
|
let layoutDirection: LayoutDirection = "horizontal";
|
||||||
let itemGridRatio = 1;
|
let itemGridRatio = 1;
|
||||||
|
|
||||||
if (presenterTileCount === 0) {
|
if (focusedTileCount === 0) {
|
||||||
return { itemGridRatio, layoutDirection };
|
return { itemGridRatio, layoutDirection };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,29 +654,25 @@ function getSubGridPositions(
|
|||||||
// Sets the 'order' property on tiles based on the layout param and
|
// Sets the 'order' property on tiles based on the layout param and
|
||||||
// other properties of the tiles, eg. 'focused' and 'presenter'
|
// other properties of the tiles, eg. 'focused' and 'presenter'
|
||||||
function reorderTiles(tiles: Tile[], layout: Layout) {
|
function reorderTiles(tiles: Tile[], layout: Layout) {
|
||||||
if (layout === "freedom" && tiles.length === 2) {
|
if (
|
||||||
|
layout === "freedom" &&
|
||||||
|
tiles.length === 2 &&
|
||||||
|
!tiles.some((t) => t.presenter)
|
||||||
|
) {
|
||||||
// 1:1 layout
|
// 1:1 layout
|
||||||
tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1));
|
tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1));
|
||||||
} else {
|
} else {
|
||||||
const focusedTiles: Tile[] = [];
|
const focusedTiles: Tile[] = [];
|
||||||
const presenterTiles: Tile[] = [];
|
|
||||||
const otherTiles: Tile[] = [];
|
const otherTiles: Tile[] = [];
|
||||||
|
|
||||||
const orderedTiles: Tile[] = new Array(tiles.length);
|
const orderedTiles: Tile[] = new Array(tiles.length);
|
||||||
tiles.forEach((tile) => (orderedTiles[tile.order] = tile));
|
tiles.forEach((tile) => (orderedTiles[tile.order] = tile));
|
||||||
|
|
||||||
orderedTiles.forEach((tile) =>
|
orderedTiles.forEach((tile) =>
|
||||||
(tile.focused
|
(tile.focused ? focusedTiles : otherTiles).push(tile)
|
||||||
? focusedTiles
|
|
||||||
: tile.presenter
|
|
||||||
? presenterTiles
|
|
||||||
: otherTiles
|
|
||||||
).push(tile)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
[...focusedTiles, ...presenterTiles, ...otherTiles].forEach(
|
[...focusedTiles, ...otherTiles].forEach((tile, i) => (tile.order = i));
|
||||||
(tile, i) => (tile.order = i)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,11 +746,8 @@ export function VideoGrid({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let focused: boolean;
|
let focused: boolean;
|
||||||
let presenter = false;
|
|
||||||
|
|
||||||
if (layout === "spotlight") {
|
if (layout === "spotlight") {
|
||||||
focused = item.focused;
|
focused = item.focused;
|
||||||
presenter = item.presenter;
|
|
||||||
} else {
|
} else {
|
||||||
focused = layout === lastLayoutRef.current ? tile.focused : false;
|
focused = layout === lastLayoutRef.current ? tile.focused : false;
|
||||||
}
|
}
|
||||||
@@ -774,7 +758,7 @@ export function VideoGrid({
|
|||||||
item,
|
item,
|
||||||
remove,
|
remove,
|
||||||
focused,
|
focused,
|
||||||
presenter,
|
presenter: item.presenter,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,7 +779,7 @@ export function VideoGrid({
|
|||||||
item,
|
item,
|
||||||
remove: false,
|
remove: false,
|
||||||
focused: layout === "spotlight" && item.focused,
|
focused: layout === "spotlight" && item.focused,
|
||||||
presenter: layout === "spotlight" && item.presenter,
|
presenter: item.presenter,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingTile) {
|
if (existingTile) {
|
||||||
@@ -821,7 +805,7 @@ export function VideoGrid({
|
|||||||
.map((tile) => ({ ...tile })); // clone before reordering
|
.map((tile) => ({ ...tile })); // clone before reordering
|
||||||
reorderTiles(newTiles, layout);
|
reorderTiles(newTiles, layout);
|
||||||
|
|
||||||
const presenterTileCount = newTiles.reduce(
|
const focusedTileCount = newTiles.reduce(
|
||||||
(count, tile) => count + (tile.focused ? 1 : 0),
|
(count, tile) => count + (tile.focused ? 1 : 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
@@ -831,7 +815,8 @@ export function VideoGrid({
|
|||||||
tiles: newTiles,
|
tiles: newTiles,
|
||||||
tilePositions: getTilePositions(
|
tilePositions: getTilePositions(
|
||||||
newTiles.length,
|
newTiles.length,
|
||||||
presenterTileCount,
|
focusedTileCount,
|
||||||
|
newTiles.some((t) => t.presenter),
|
||||||
gridBounds.width,
|
gridBounds.width,
|
||||||
gridBounds.height,
|
gridBounds.height,
|
||||||
pipXRatio,
|
pipXRatio,
|
||||||
@@ -843,7 +828,7 @@ export function VideoGrid({
|
|||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
const presenterTileCount = newTiles.reduce(
|
const focusedTileCount = newTiles.reduce(
|
||||||
(count, tile) => count + (tile.focused ? 1 : 0),
|
(count, tile) => count + (tile.focused ? 1 : 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
@@ -855,7 +840,8 @@ export function VideoGrid({
|
|||||||
tiles: newTiles,
|
tiles: newTiles,
|
||||||
tilePositions: getTilePositions(
|
tilePositions: getTilePositions(
|
||||||
newTiles.length,
|
newTiles.length,
|
||||||
presenterTileCount,
|
focusedTileCount,
|
||||||
|
newTiles.some((t) => t.presenter),
|
||||||
gridBounds.width,
|
gridBounds.width,
|
||||||
gridBounds.height,
|
gridBounds.height,
|
||||||
pipXRatio,
|
pipXRatio,
|
||||||
@@ -959,7 +945,7 @@ export function VideoGrid({
|
|||||||
const item = tile.item;
|
const item = tile.item;
|
||||||
|
|
||||||
setTileState(({ tiles, ...state }) => {
|
setTileState(({ tiles, ...state }) => {
|
||||||
let presenterTileCount = 0;
|
let focusedTileCount = 0;
|
||||||
const newTiles = tiles.map((tile) => {
|
const newTiles = tiles.map((tile) => {
|
||||||
const newTile = { ...tile }; // clone before reordering
|
const newTile = { ...tile }; // clone before reordering
|
||||||
|
|
||||||
@@ -967,7 +953,7 @@ export function VideoGrid({
|
|||||||
newTile.focused = !tile.focused;
|
newTile.focused = !tile.focused;
|
||||||
}
|
}
|
||||||
if (newTile.focused) {
|
if (newTile.focused) {
|
||||||
presenterTileCount++;
|
focusedTileCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTile;
|
return newTile;
|
||||||
@@ -980,7 +966,8 @@ export function VideoGrid({
|
|||||||
tiles: newTiles,
|
tiles: newTiles,
|
||||||
tilePositions: getTilePositions(
|
tilePositions: getTilePositions(
|
||||||
newTiles.length,
|
newTiles.length,
|
||||||
presenterTileCount,
|
focusedTileCount,
|
||||||
|
newTiles.some((t) => t.presenter),
|
||||||
gridBounds.width,
|
gridBounds.width,
|
||||||
gridBounds.height,
|
gridBounds.height,
|
||||||
pipXRatio,
|
pipXRatio,
|
||||||
@@ -1012,7 +999,7 @@ export function VideoGrid({
|
|||||||
|
|
||||||
let newTiles = tiles;
|
let newTiles = tiles;
|
||||||
|
|
||||||
if (tiles.length === 2) {
|
if (tiles.length === 2 && !tiles.some((t) => t.presenter)) {
|
||||||
// We're in 1:1 mode, so only the local tile should be draggable
|
// We're in 1:1 mode, so only the local tile should be draggable
|
||||||
if (!dragTile.item.isLocal) return;
|
if (!dragTile.item.isLocal) return;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import { animated } from "@react-spring/web";
|
import { animated } from "@react-spring/web";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./VideoTile.module.css";
|
import styles from "./VideoTile.module.css";
|
||||||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
||||||
@@ -66,6 +67,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const toolbarButtons: JSX.Element[] = [];
|
const toolbarButtons: JSX.Element[] = [];
|
||||||
if (!isLocal) {
|
if (!isLocal) {
|
||||||
toolbarButtons.push(
|
toolbarButtons.push(
|
||||||
@@ -111,7 +114,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
{!maximised &&
|
{!maximised &&
|
||||||
(screenshare ? (
|
(screenshare ? (
|
||||||
<div className={styles.presenterLabel}>
|
<div className={styles.presenterLabel}>
|
||||||
<span>{`${name} is presenting`}</span>
|
<span>{t("{{name}} is presenting", { name })}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
<div className={classNames(styles.infoBubble, styles.memberName)}>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, { ChangeEvent, useState } from "react";
|
import React, { ChangeEvent, useState } from "react";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { FieldRow } from "../input/Input";
|
import { FieldRow } from "../input/Input";
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
@@ -61,10 +62,12 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const VideoTileSettingsModal = ({ feed, ...rest }: Props) => {
|
export const VideoTileSettingsModal = ({ feed, ...rest }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={styles.videoTileSettingsModal}
|
className={styles.videoTileSettingsModal}
|
||||||
title="Local volume"
|
title={t("Local volume")}
|
||||||
isDismissable
|
isDismissable
|
||||||
mobileFullScreen
|
mobileFullScreen
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { WidgetApi, MatrixCapabilities } from "matrix-widget-api";
|
|||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import { LazyEventEmitter } from "./LazyEventEmitter";
|
import { LazyEventEmitter } from "./LazyEventEmitter";
|
||||||
import { getRoomParams } from "./room/useRoomParams";
|
import { getUrlParams } from "./UrlParams";
|
||||||
|
|
||||||
// Subset of the actions in matrix-react-sdk
|
// Subset of the actions in matrix-react-sdk
|
||||||
export enum ElementWidgetActions {
|
export enum ElementWidgetActions {
|
||||||
@@ -80,10 +80,11 @@ export const widget: WidgetHelpers | null = (() => {
|
|||||||
// We need to do this now rather than later because it has capabilities to
|
// We need to do this now rather than later because it has capabilities to
|
||||||
// request, and is responsible for starting the transport (should it be?)
|
// request, and is responsible for starting the transport (should it be?)
|
||||||
|
|
||||||
const { roomId, userId, deviceId } = getRoomParams();
|
const { roomId, userId, deviceId, baseUrl } = getUrlParams();
|
||||||
if (!roomId) throw new Error("Room ID must be supplied");
|
if (!roomId) throw new Error("Room ID must be supplied");
|
||||||
if (!userId) throw new Error("User ID must be supplied");
|
if (!userId) throw new Error("User ID must be supplied");
|
||||||
if (!deviceId) throw new Error("Device ID must be supplied");
|
if (!deviceId) throw new Error("Device ID must be supplied");
|
||||||
|
if (!baseUrl) throw new Error("Base URL must be supplied");
|
||||||
|
|
||||||
// These are all the event types the app uses
|
// These are all the event types the app uses
|
||||||
const sendState = [
|
const sendState = [
|
||||||
@@ -119,7 +120,7 @@ export const widget: WidgetHelpers | null = (() => {
|
|||||||
},
|
},
|
||||||
roomId,
|
roomId,
|
||||||
{
|
{
|
||||||
baseUrl: "",
|
baseUrl,
|
||||||
userId,
|
userId,
|
||||||
deviceId,
|
deviceId,
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
svgrPlugin(),
|
svgrPlugin(),
|
||||||
htmlTemplate({
|
htmlTemplate.default({
|
||||||
data: {
|
data: {
|
||||||
title: env.VITE_PRODUCT_NAME || "Matrix Video Chat",
|
title: env.VITE_PRODUCT_NAME || "Matrix Video Chat",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user