Compare commits

...

85 Commits

Author SHA1 Message Date
David Baker
54fe2aa7a3 Bump js-sdk for addTransceiver fix 2022-10-19 18:06:24 +01:00
David Baker
3ff201562b Bump js-sdk 2022-10-19 16:02:48 +01:00
Robin
e139ac6584 Merge pull request #635 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2022-10-19 09:18:55 -04:00
Glandos
85210df28e Translated using Weblate (French)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2022-10-19 06:57:34 +00:00
Rodion Borisov
0af116ce76 Translated using Weblate (Russian)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ru/
2022-10-19 06:57:34 +00:00
Vri
a09bb109fd Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-19 06:57:34 +00:00
Nui Harime
c97185a50e Added translation using Weblate (Ukrainian) 2022-10-19 06:57:34 +00:00
Robin
50f7fedfa0 Merge pull request #632 from robintown/freedom-screenshare
Prevent screenshare feeds from collapsing when you're alone in freedom mode
2022-10-18 11:58:53 -04:00
Robin Townsend
178c6496bd yarn prettier:format 2022-10-18 00:48:29 -04:00
Robin Townsend
c5eb9f0b99 Prevent screenshare feeds from collapsing when you're alone in freedom mode
The code was previously confusing focused and presenter tiles quite a bit, and also had a couple different spots that would mistakenly engage 1:1 layout behavior when you're alone with your own screensharing feed.
2022-10-18 00:30:37 -04:00
Robin
af4c1280f5 Merge pull request #631 from robintown/fix-fullscreen
Fix fullscreen buttons fullscreening the wrong feed
2022-10-17 14:53:31 -04:00
Robin Townsend
97ae11f656 Fix fullscreen buttons fullscreening the wrong feed 2022-10-17 12:31:56 -04:00
Robin
e182dd50f2 Merge pull request #630 from robintown/matryoshka-baseurl
Make avatars work in matryoshka mode
2022-10-17 09:32:30 -04:00
Robin Townsend
43f98e6be6 yarn prettier:format 2022-10-17 09:30:22 -04:00
Robin Townsend
70ba6c3c6b Make avatars work in matryoshka mode
The client just didn't have a homeserver URL to perform media queries against.
2022-10-17 01:46:44 -04:00
Robin
29a7376bc7 Merge pull request #629 from RiotTranslateBot/weblate-element-call-element-call
Translations update from Weblate
2022-10-17 01:11:07 -04:00
Robin Townsend
db02178fce Revert en-GB changes 2022-10-17 01:07:43 -04:00
Robin Townsend
1d69bef7f9 Remove country codes from most language codes 2022-10-17 01:07:25 -04:00
fkwp
0a83a8804f Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-17 04:54:35 +00:00
Thibault Martin
5795e20865 Translated using Weblate (French)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2022-10-15 13:59:22 +00:00
Robin Townsend
4aba1c8b74 Translated using Weblate (Indonesian)
Currently translated at 99.2% (130 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2022-10-15 13:59:22 +00:00
Robin Townsend
dc694d4ffe Translated using Weblate (Turkish)
Currently translated at 77.0% (101 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/tr/
2022-10-15 13:59:21 +00:00
Robin Townsend
fafc56bb90 Translated using Weblate (Korean)
Currently translated at 5.3% (7 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ko/
2022-10-15 13:59:21 +00:00
fkwp
a83611c287 Translated using Weblate (German)
Currently translated at 100.0% (131 of 131 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:59:21 +00:00
Weblate
2cca320291 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/
2022-10-15 13:21:32 +00:00
Thibault Martin
834582a870 Translated using Weblate (French)
Currently translated at 96.2% (128 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2022-10-15 13:21:31 +00:00
fkwp
2390b990c5 Translated using Weblate (German)
Currently translated at 98.4% (131 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
kongo09
166046a4b1 Translated using Weblate (German)
Currently translated at 98.4% (131 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Thibault Martin
f2dbe8abbe Translated using Weblate (French)
Currently translated at 75.1% (100 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/fr/
2022-10-15 13:21:31 +00:00
Linerly
1a814713df Translated using Weblate (Indonesian)
Currently translated at 0.0% (0 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/id/
2022-10-15 13:21:31 +00:00
Erkin Alp Güney
fceb10e2df Translated using Weblate (Turkish)
Currently translated at 0.0% (0 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/tr/
2022-10-15 13:21:31 +00:00
Youngbin Han
94323b3597 Translated using Weblate (Korean)
Currently translated at 0.0% (0 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/ko/
2022-10-15 13:21:31 +00:00
Vri
a8c5cb4821 Translated using Weblate (German)
Currently translated at 97.7% (130 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
fkwp
6e32aad729 Translated using Weblate (German)
Currently translated at 97.7% (130 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
fkwp
49f6249144 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (133 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/en_GB/
2022-10-15 13:21:31 +00:00
Thibault Martin
ab08b58ef5 Added translation using Weblate (French) 2022-10-15 13:21:31 +00:00
Nui Harime
ba9efc64c3 Added translation using Weblate (Russian) 2022-10-15 13:21:31 +00:00
Slavi Pantaleev
e986ef914f Translated using Weblate (Bulgarian)
Currently translated at 99.2% (132 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/bg/
2022-10-15 13:21:31 +00:00
Erkin Alp Güney
68117cd9e4 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (133 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/en_GB/
2022-10-15 13:21:31 +00:00
Slavi Pantaleev
ccb4f8c0e4 Added translation using Weblate (Bulgarian) 2022-10-15 13:21:31 +00:00
Linerly
1367a50b75 Added translation using Weblate (Indonesian) 2022-10-15 13:21:31 +00:00
Erkin Alp Güney
aec21e661d Added translation using Weblate (Turkish) 2022-10-15 13:21:31 +00:00
Youngbin Han
ae7697b33c Added translation using Weblate (Korean) 2022-10-15 13:21:31 +00:00
Robin Townsend
37f72fe0b6 Translated using Weblate (German)
Currently translated at 94.7% (126 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Johannes Marbach
5660938f47 Translated using Weblate (German)
Currently translated at 94.7% (126 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
kongo09
1c76385d79 Translated using Weblate (German)
Currently translated at 94.7% (126 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
fkwp
208a3d9045 Translated using Weblate (German)
Currently translated at 94.7% (126 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Travis Ralston
16c9483f37 Deleted translation using Weblate (English (United States)) 2022-10-15 13:21:31 +00:00
Travis Ralston
70939fa8f0 Added translation using Weblate (English (United States)) 2022-10-15 13:21:31 +00:00
Robin Townsend
ec1f846c92 Translated using Weblate (German)
Currently translated at 25.5% (34 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
fkwp
1570657176 Translated using Weblate (German)
Currently translated at 25.5% (34 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Timo
7e78f7a670 Translated using Weblate (German)
Currently translated at 25.5% (34 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Robin Townsend
d556fe188a Translated using Weblate (German)
Currently translated at 16.5% (22 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Johannes Marbach
c07aeb3ba8 Translated using Weblate (German)
Currently translated at 16.5% (22 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Timo
a6c6aed61c Translated using Weblate (German)
Currently translated at 16.5% (22 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Robin Townsend
28a20d9b1e Translated using Weblate (German)
Currently translated at 16.5% (22 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Johannes Marbach
077e361a26 Translated using Weblate (German)
Currently translated at 12.7% (17 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Timo
6180f2e1b9 Translated using Weblate (German)
Currently translated at 12.7% (17 of 133 strings)

Translation: Element Call/element-call
Translate-URL: https://translate.element.io/projects/element-call/element-call/de/
2022-10-15 13:21:31 +00:00
Robin
5e57a56d21 Merge pull request #627 from robintown/dedup-strings
Consolidate some similar strings
2022-10-15 09:16:45 -04:00
Robin Townsend
402f62e09a Consolidate some similar strings 2022-10-14 18:38:33 -04:00
Robin
6ec2e9c822 Merge pull request #621 from robintown/hide-invite
Hide the invite button in non-public rooms
2022-10-14 10:56:22 -04:00
Robin Townsend
684defdc19 Merge branch 'main' into hide-invite 2022-10-14 10:51:41 -04:00
Robin Townsend
5ed2dc6e0e Split room state hooks out into separate files 2022-10-14 10:50:36 -04:00
Šimon Brandner
ce86a6f120 Merge pull request #622 from vector-im/SimonBrandner/feat/hide-screen 2022-10-14 16:49:54 +02:00
Šimon Brandner
96b1a5f296 hideScreensharing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-10-14 16:17:50 +02:00
Robin
e9ebccf0df Merge pull request #619 from robintown/unmute
Leave audio elements unmuted regardless of mute state
2022-10-14 09:59:54 -04:00
Robin Townsend
02b2aef958 Hide the invite button in non-public rooms 2022-10-14 09:40:21 -04:00
Robin
c6d60cff64 Merge pull request #620 from robintown/update-js-sdk
Update matrix-js-sdk
2022-10-14 08:08:13 -04:00
Robin Townsend
81771f511c Fix types 2022-10-13 21:25:15 -04:00
Robin Townsend
004160b664 Update matrix-js-sdk 2022-10-13 20:24:48 -04:00
Robin
2d25d3c2bc Merge pull request #618 from robintown/i18n
Set up translation with i18next
2022-10-13 19:53:43 -04:00
Robin Townsend
4728804a33 Leave audio elements unmuted regardless of mute state 2022-10-13 10:49:16 -04:00
Robin Townsend
8524b9ecd6 Set up translation with i18next 2022-10-12 14:53:49 -04:00
David Baker
eca598e28f Merge pull request #609 from vector-im/dbkr/device_by_name
Use device labels rather than IDs in widget API
2022-09-30 17:28:59 +01:00
David Baker
f808c56121 Type 2022-09-29 17:08:48 +01:00
David Baker
77da0c912f Match device type too
Because lots of audio & video inputs have the same name
2022-09-29 17:07:10 +01:00
Šimon Brandner
e8a875eb32 Merge pull request #610 from vector-im/SimonBrandner/task/update-js 2022-09-29 16:11:00 +02:00
Šimon Brandner
e7a94426c2 Update js-sdk (again)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-29 15:20:55 +02:00
David Baker
17613837b6 Hold a user media stream open while we get devices
As per comment.
2022-09-29 13:19:46 +01:00
Šimon Brandner
4b4c98066c Update js-sdk
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-28 18:15:34 +02:00
David Baker
4a5b69800c Use device labels rather than IDs in widget API
device IDs are different for each origin, so won't match up when passed
in & out of widgets. Use the label instead.

For https://github.com/vector-im/element-web/issues/23331
2022-09-27 16:19:48 +01:00
Robin
70d6c3e9bf Merge pull request #608 from robintown/reduced-controls
Show a reduced set of controls when the window is narrow
2022-09-27 07:54:24 -04:00
Robin
90e32af220 Merge pull request #607 from robintown/tooltip-style
Make tooltips look more like Element Web's
2022-09-27 07:54:14 -04:00
Robin Townsend
fdc0272940 Show a reduced set of controls when the window is narrow 2022-09-26 20:36:51 -04:00
Robin Townsend
d90a837714 Make tooltips look more like Element Web's
The only thing they're missing now is animated fading.
2022-09-26 20:17:55 -04:00
75 changed files with 2581 additions and 696 deletions

20
i18next-parser.config.js Normal file
View 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,
};

View File

@@ -1,5 +1,6 @@
{
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
@@ -10,7 +11,8 @@
"prettier:format": "prettier -w src",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src",
"lint:types": "tsc"
"lint:types": "tsc",
"i18n": "node_modules/i18next-parser/bin/cli.js"
},
"dependencies": {
"@juggle/resize-observer": "^3.3.1",
@@ -38,7 +40,10 @@
"classnames": "^2.3.1",
"color-hash": "^2.0.1",
"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",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",
@@ -47,6 +52,7 @@
"re-resizable": "^6.9.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-i18next": "^11.18.6",
"react-json-view": "^1.21.3",
"react-router": "6",
"react-router-dom": "^5.2.0",
@@ -71,6 +77,7 @@
"eslint-plugin-matrix-org": "^0.4.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.5.0",
"i18next-parser": "^6.6.0",
"prettier": "^2.6.2",
"sass": "^1.42.1",
"storybook-builder-vite": "^0.1.12",

132
public/locales/bg/app.json Normal file
View 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
View 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 …"
}

View 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
View 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 laccès à votre caméra et microphone pour rejoindre lappel.",
"Accept microphone permissions to join the call.": "Autorisez laccès au microphone pour rejoindre lappel.",
"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 dutilisation</2>",
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "En cliquant sur « Rejoindre lappel » vous acceptez nos <2>conditions dutilisation</2>",
"Call link copied": "Lien de lappel copié",
"Call type menu": "Menu de type dappel",
"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 lappel.",
"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 dun journal de débogage",
"Description (optional)": "Description (facultatif)",
"Details": "Informations",
"Developer": "Développeur",
"Display name": "Nom daffichage",
"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 lappel",
"Join call now": "Rejoindre lappel 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 lappel.",
"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 à laccueil",
"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 despace enfoncée pour parler",
"Press and hold spacebar to talk over {{name}}": "Appuyez et maintenez la barre despace 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": "Senregistrer",
"Registering…": "Enregistrement…",
"Release spacebar key to stop": "Relâcher la barre despace pour arrêter",
"Release to stop": "Relâcher pour arrêter",
"Remove": "Supprimer",
"Return to home screen": "Retour à laccueil",
"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 linspecteur dappel",
"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 à laccueil",
"Talk over speaker": "Parler par dessus lintervenant",
"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 à lappel 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 lappel",
"WebRTC is not supported or is being blocked in this browser.": "WebRTC nest pas pris en charge ou est bloqué par ce navigateur.",
"Walkie-talkie call name": "Nom de lappel talkie-walkie",
"Walkie-talkie call": "Appel talkie-walkie",
"Waiting for other participants…": "En attente dautres participants…",
"Waiting for network": "En attente du réseau",
"Video call name": "Nom de lappel vidéo",
"Video call": "Appel vidéo",
"Video": "Vidéo",
"Version: {{version}}": "Version : {{version}}",
"Username": "Nom dutilisateur",
"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 limpression que le son de lintervenant 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 dutilisation</6> de Google sappliquent.<9></9>En cliquant sur « Senregistrer » vous acceptez également nos <12>conditions dutilisation</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
View 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"
}

View 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
View 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
View 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>"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { Suspense } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import * as Sentry from "@sentry/react";
import { OverlayProvider } from "@react-aria/overlays";
@@ -43,34 +43,36 @@ export default function App({ history }: AppProps) {
return (
<Router history={history}>
<ClientProvider>
<InspectorContextProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<OverlayProvider>
<Switch>
<SentryRoute exact path="/">
<HomePage />
</SentryRoute>
<SentryRoute exact path="/login">
<LoginPage />
</SentryRoute>
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="/room/:roomId?">
<RoomPage />
</SentryRoute>
<SentryRoute path="/inspector">
<SequenceDiagramViewerPage />
</SentryRoute>
<SentryRoute path="*">
<RoomRedirect />
</SentryRoute>
</Switch>
</OverlayProvider>
</Sentry.ErrorBoundary>
</InspectorContextProvider>
</ClientProvider>
<Suspense fallback={null}>
<ClientProvider>
<InspectorContextProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<OverlayProvider>
<Switch>
<SentryRoute exact path="/">
<HomePage />
</SentryRoute>
<SentryRoute exact path="/login">
<LoginPage />
</SentryRoute>
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="/room/:roomId?">
<RoomPage />
</SentryRoute>
<SentryRoute path="/inspector">
<SequenceDiagramViewerPage />
</SentryRoute>
<SentryRoute path="*">
<RoomRedirect />
</SentryRoute>
</Switch>
</OverlayProvider>
</Sentry.ErrorBoundary>
</InspectorContextProvider>
</ClientProvider>
</Suspense>
</Router>
);
}

View File

@@ -27,6 +27,7 @@ import { useHistory } from "react-router-dom";
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next";
import { ErrorView } from "./FullScreenView";
import {
@@ -35,6 +36,7 @@ import {
CryptoStoreIntegrityError,
} from "./matrix-utils";
import { widget } from "./widget";
import { translatedError } from "./TranslatedError";
declare global {
interface Window {
@@ -146,7 +148,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
},
false // Don't need the crypto store just to log out
);
await client.logout(undefined, true);
await client.logout(true);
} catch (err_) {
logger.warn(
"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 () => {
await client.logout(undefined, true);
await client.logout(true);
await client.clearStores();
clearSession();
setState({
@@ -267,6 +269,8 @@ export const ClientProvider: FC<Props> = ({ children }) => {
history.push("/");
}, [history, client]);
const { t } = useTranslation();
useEffect(() => {
// To protect against multiple sessions writing to the same storage
// simultaneously, we send a to-device message that shuts down all other
@@ -287,8 +291,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
setState((prev) => ({
...prev,
error: new Error(
"This application has been opened in another tab."
error: translatedError(
"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]);
}, [client, t]);
const context = useMemo<ClientState>(
() => ({

View File

@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { HTMLAttributes } from "react";
import React, { HTMLAttributes, useMemo } from "react";
import classNames from "classnames";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useTranslation } from "react-i18next";
import styles from "./Facepile.module.css";
import { Avatar, Size, sizes } from "./Avatar";
@@ -44,13 +45,25 @@ export function Facepile({
size = Size.XS,
...rest
}: Props) {
const { t } = useTranslation();
const _size = sizes.get(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 (
<div
className={classNames(styles.facepile, styles[size], className)}
title={participants.map((member) => member.name).join(", ")}
title={title}
style={{
width:
Math.min(participants.length, max + 1) * (_size - _overlap) +

View File

@@ -1,12 +1,14 @@
import React, { ReactNode, useCallback, useEffect } from "react";
import { useLocation } from "react-router-dom";
import classNames from "classnames";
import { Trans, useTranslation } from "react-i18next";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import { LinkButton, Button } from "./button";
import { useSubmitRageshake } from "./settings/submit-rageshake";
import { ErrorMessage } from "./input/Input";
import styles from "./FullScreenView.module.css";
import { translatedError, TranslatedError } from "./TranslatedError";
interface FullScreenViewProps {
className?: string;
@@ -35,6 +37,7 @@ interface ErrorViewProps {
export function ErrorView({ error }: ErrorViewProps) {
const location = useLocation();
const { t } = useTranslation();
useEffect(() => {
console.error(error);
@@ -47,7 +50,11 @@ export function ErrorView({ error }: ErrorViewProps) {
return (
<FullScreenView>
<h1>Error</h1>
<p>{error.message}</p>
<p>
{error instanceof TranslatedError
? error.translatedMessage
: error.message}
</p>
{location.pathname === "/" ? (
<Button
size="lg"
@@ -55,7 +62,7 @@ export function ErrorView({ error }: ErrorViewProps) {
className={styles.homeLink}
onPress={onReload}
>
Return to home screen
{t("Return to home screen")}
</Button>
) : (
<LinkButton
@@ -64,7 +71,7 @@ export function ErrorView({ error }: ErrorViewProps) {
className={styles.homeLink}
to="/"
>
Return to home screen
{t("Return to home screen")}
</LinkButton>
)}
</FullScreenView>
@@ -72,6 +79,7 @@ export function ErrorView({ error }: ErrorViewProps) {
}
export function CrashView() {
const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendDebugLogs = useCallback(() => {
@@ -85,11 +93,11 @@ export function CrashView() {
window.location.href = "/";
}, []);
let logsComponent;
let logsComponent: JSX.Element | null = null;
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) {
logsComponent = <div>Sending...</div>;
logsComponent = <div>{t("Sending…")}</div>;
} else {
logsComponent = (
<Button
@@ -98,33 +106,39 @@ export function CrashView() {
onPress={sendDebugLogs}
className={styles.wideButton}
>
Send debug logs
{t("Send debug logs")}
</Button>
);
}
return (
<FullScreenView>
<h1>Oops, something's gone wrong.</h1>
<p>Submitting debug logs will help us track down the problem.</p>
<Trans>
<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>
{error && <ErrorMessage>Couldn't send debug logs!</ErrorMessage>}
{error && (
<ErrorMessage error={translatedError("Couldn't send debug logs!", t)} />
)}
<Button
size="lg"
variant="default"
className={styles.wideButton}
onPress={onReload}
>
Return to home screen
{t("Return to home screen")}
</Button>
</FullScreenView>
);
}
export function LoadingView() {
const { t } = useTranslation();
return (
<FullScreenView>
<h1>Loading...</h1>
<h1>{t("Loading")}</h1>
</FullScreenView>
);
}

View File

@@ -4,6 +4,7 @@ import { Link } from "react-router-dom";
import { useButton } from "@react-aria/button";
import { AriaButtonProps } from "@react-types/button";
import { Room } from "matrix-js-sdk/src/models/room";
import { useTranslation } from "react-i18next";
import styles from "./Header.module.css";
import { useModalTriggerState } from "./Modal";
@@ -156,6 +157,7 @@ export function VersionMismatchWarning({
users,
room,
}: VersionMismatchWarningProps) {
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
const onDetailsClick = useCallback(() => {
@@ -166,9 +168,9 @@ export function VersionMismatchWarning({
return (
<span className={styles.versionMismatchWarning}>
Incomaptible versions!
{t("Incompatible versions!")}
<Button variant="link" onClick={onDetailsClick}>
Details
{t("Details")}
</Button>
{modalState.isOpen && (
<IncompatibleVersionModal userIds={users} room={room} {...modalProps} />

View File

@@ -15,7 +15,8 @@ limitations under the License.
*/
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 { Body } from "./typography/Typography";
@@ -30,17 +31,21 @@ export const IncompatibleVersionModal: React.FC<Props> = ({
room,
...rest
}) => {
const userLis = Array.from(userIds).map((u) => (
<li>{room.getMember(u).name}</li>
));
const { t } = useTranslation();
const userLis = useMemo(
() => [...userIds].map((u) => <li>{room.getMember(u)?.name ?? u}</li>),
[userIds, room]
);
return (
<Modal title="Incompatible Versions" isDismissable {...rest}>
<Modal title={t("Incompatible versions")} isDismissable {...rest}>
<ModalContent>
<Body>
Other users are trying to join this call from incompatible versions.
These users should ensure that they have refreshed their browsers:
<ul>{userLis}</ul>
<Trans>
Other users are trying to join this call from incompatible versions.
These users should ensure that they have refreshed their browsers:
<ul>{userLis}</ul>
</Trans>
</Body>
</ModalContent>
</Modal>

View File

@@ -33,6 +33,7 @@ import { FocusScope } from "@react-aria/focus";
import { ButtonAria, useButton } from "@react-aria/button";
import classNames from "classnames";
import { AriaDialogProps } from "@react-types/dialog";
import { useTranslation } from "react-i18next";
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
import styles from "./Modal.module.css";
@@ -53,6 +54,7 @@ export function Modal({
onClose,
...rest
}: ModalProps) {
const { t } = useTranslation();
const modalRef = useRef();
const { overlayProps, underlayProps } = useOverlay(
{ ...rest, onClose },
@@ -90,6 +92,7 @@ export function Modal({
{...closeButtonProps}
ref={closeButtonRef}
className={styles.closeButton}
title={t("Close")}
>
<CloseIcon />
</button>

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import {
SequenceDiagramViewer,
@@ -30,7 +31,8 @@ interface DebugLog {
}
export function SequenceDiagramViewerPage() {
usePageTitle("Inspector");
const { t } = useTranslation();
usePageTitle(t("Inspector"));
const [debugLog, setDebugLog] = useState<DebugLog>();
const [selectedUserId, setSelectedUserId] = useState<string>();
@@ -49,7 +51,7 @@ export function SequenceDiagramViewerPage() {
type="file"
id="debugLog"
name="debugLog"
label="Debug Log"
label={t("Debug log")}
onChange={onChangeDebugLog}
/>
</FieldRow>

View File

@@ -3,10 +3,12 @@
flex-direction: row;
justify-content: center;
align-items: center;
padding: 8px 10px;
padding: 10px;
color: var(--primary-content);
border-radius: 8px;
max-width: 135px;
width: max-content;
font-size: 12px;
font-weight: 500;
text-align: center;
}

View File

@@ -86,7 +86,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
targetRef: triggerRef,
overlayRef,
isOpen: tooltipState.isOpen,
offset: 5,
offset: 12,
});
return (

41
src/TranslatedError.ts Normal file
View 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);

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { useMemo } from "react";
import { useLocation } from "react-router-dom";
export interface RoomParams {
export interface UrlParams {
roomAlias: string | null;
roomId: string | null;
viaServers: string[];
@@ -29,35 +29,41 @@ export interface RoomParams {
preload: boolean;
// Whether to hide the room header when in a call
hideHeader: boolean;
// Whether to hide the screen-sharing button
hideScreensharing: boolean;
// Whether to start a walkie-talkie call instead of a video call
isPtt: boolean;
// Whether to use end-to-end encryption
e2eEnabled: boolean;
// The user's ID (only used in Matroska mode)
// The user's ID (only used in matryoshka mode)
userId: string | null;
// The display name to use for auto-registration
displayName: string | null;
// The device's ID (only used in Matroska mode)
// The device's ID (only used in matryoshka mode)
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.
* @param {string} query The URL query string
* @param {string} fragment The URL fragment string
* @returns {RoomParams} The room parameters encoded in the URL
* Gets the app parameters for the current URL.
* @param query The URL query string
* @param fragment The URL fragment string
* @returns The app parameters encoded in the URL
*/
export const getRoomParams = (
export const getUrlParams = (
query: string = window.location.search,
fragment: string = window.location.hash
): RoomParams => {
): UrlParams => {
const fragmentQueryStart = fragment.indexOf("?");
const fragmentParams = new URLSearchParams(
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
);
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
// string for backwards compatibility with versions that only used that.
const hasParam = (name: string): boolean =>
@@ -82,19 +88,22 @@ export const getRoomParams = (
isEmbedded: hasParam("embed"),
preload: hasParam("preload"),
hideHeader: hasParam("hideHeader"),
hideScreensharing: hasParam("hideScreensharing"),
isPtt: hasParam("ptt"),
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
userId: getParam("userId"),
displayName: getParam("displayName"),
deviceId: getParam("deviceId"),
baseUrl: getParam("baseUrl"),
lang: getParam("lang"),
};
};
/**
* Hook to simplify use of getRoomParams.
* @returns {RoomParams} The room parameters for the current URL
* Hook to simplify use of getUrlParams.
* @returns The app parameters for the current URL
*/
export const useRoomParams = (): RoomParams => {
export const useUrlParams = (): UrlParams => {
const { hash, search } = useLocation();
return useMemo(() => getRoomParams(search, hash), [search, hash]);
return useMemo(() => getUrlParams(search, hash), [search, hash]);
};

View File

@@ -1,6 +1,7 @@
import React, { useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import { Item } from "@react-stately/collections";
import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Button, LinkButton } from "./button";
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
@@ -30,6 +31,7 @@ export function UserMenu({
avatarUrl,
onAction,
}: UserMenuProps) {
const { t } = useTranslation();
const location = useLocation();
const items = useMemo(() => {
@@ -45,7 +47,7 @@ export function UserMenu({
if (isPasswordlessUser && !preventNavigation) {
arr.push({
key: "login",
label: "Sign In",
label: t("Sign in"),
icon: LoginIcon,
});
}
@@ -53,14 +55,16 @@ export function UserMenu({
if (!isPasswordlessUser && !preventNavigation) {
arr.push({
key: "logout",
label: "Sign Out",
label: t("Sign out"),
icon: LogoutIcon,
});
}
}
return arr;
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation]);
}, [isAuthenticated, isPasswordlessUser, displayName, preventNavigation, t]);
const tooltip = useCallback(() => t("Profile"), [t]);
if (!isAuthenticated) {
return (
@@ -72,7 +76,7 @@ export function UserMenu({
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={() => "Profile"} placement="bottom left">
<TooltipTrigger tooltip={tooltip} placement="bottom left">
<Button variant="icon" className={styles.userButton}>
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
<Avatar
@@ -87,7 +91,7 @@ export function UserMenu({
</Button>
</TooltipTrigger>
{(props) => (
<Menu {...props} label="User menu" onAction={onAction}>
<Menu {...props} label={t("User menu")} onAction={onAction}>
{items.map(({ key, icon: Icon, label }) => (
<Item key={key} textValue={label}>
<Icon width={24} height={24} className={styles.menuIcon} />

View File

@@ -23,6 +23,7 @@ import React, {
useMemo,
} from "react";
import { useHistory, useLocation, Link } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
import { useClient } from "../ClientContext";
@@ -34,7 +35,8 @@ import { useInteractiveLogin } from "./useInteractiveLogin";
import { usePageTitle } from "../usePageTitle";
export const LoginPage: FC = () => {
usePageTitle("Login");
const { t } = useTranslation();
usePageTitle(t("Login"));
const { setClient } = useClient();
const login = useInteractiveLogin();
@@ -93,8 +95,8 @@ export const LoginPage: FC = () => {
<InputField
type="text"
ref={usernameRef}
placeholder="Username"
label="Username"
placeholder={t("Username")}
label={t("Username")}
autoCorrect="off"
autoCapitalize="none"
prefix="@"
@@ -105,18 +107,18 @@ export const LoginPage: FC = () => {
<InputField
type="password"
ref={passwordRef}
placeholder="Password"
label="Password"
placeholder={t("Password")}
label={t("Password")}
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={loading}>
{loading ? "Logging in..." : "Login"}
{loading ? t("Logging in…") : t("Login")}
</Button>
</FieldRow>
</form>
@@ -124,9 +126,11 @@ export const LoginPage: FC = () => {
<div className={styles.authLinks}>
<p>Not registered yet?</p>
<p>
<Link to="/register">Create an account</Link>
{" Or "}
<Link to="/">Access as a guest</Link>
<Trans>
<Link to="/register">Create an account</Link>
{" Or "}
<Link to="/">Access as a guest</Link>
</Trans>
</p>
</div>
</div>

View File

@@ -26,6 +26,7 @@ import React, {
import { useHistory, useLocation } from "react-router-dom";
import { captureException } from "@sentry/react";
import { sleep } from "matrix-js-sdk/src/utils";
import { Trans, useTranslation } from "react-i18next";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Button } from "../button";
@@ -40,7 +41,8 @@ import { Caption, Link } from "../typography/Typography";
import { usePageTitle } from "../usePageTitle";
export const RegisterPage: FC = () => {
usePageTitle("Register");
const { t } = useTranslation();
usePageTitle(t("Register"));
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
useClient();
@@ -126,11 +128,11 @@ export const RegisterPage: FC = () => {
useEffect(() => {
if (password && passwordConfirmation && password !== passwordConfirmation) {
confirmPasswordRef.current?.setCustomValidity("Passwords must match");
confirmPasswordRef.current?.setCustomValidity(t("Passwords must match"));
} else {
confirmPasswordRef.current?.setCustomValidity("");
}
}, [password, passwordConfirmation]);
}, [password, passwordConfirmation, t]);
useEffect(() => {
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
@@ -154,8 +156,8 @@ export const RegisterPage: FC = () => {
<InputField
type="text"
name="userName"
placeholder="Username"
label="Username"
placeholder={t("Username")}
label={t("Username")}
autoCorrect="off"
autoCapitalize="none"
prefix="@"
@@ -171,8 +173,8 @@ export const RegisterPage: FC = () => {
setPassword(e.target.value)
}
value={password}
placeholder="Password"
label="Password"
placeholder={t("Password")}
label={t("Password")}
/>
</FieldRow>
<FieldRow>
@@ -184,45 +186,49 @@ export const RegisterPage: FC = () => {
setPasswordConfirmation(e.target.value)
}
value={passwordConfirmation}
placeholder="Confirm Password"
label="Confirm Password"
placeholder={t("Confirm password")}
label={t("Confirm password")}
ref={confirmPasswordRef}
/>
</FieldRow>
<Caption>
This site is protected by ReCAPTCHA and the Google{" "}
<Link href="https://www.google.com/policies/privacy/">
Privacy Policy
</Link>{" "}
and{" "}
<Link href="https://policies.google.com/terms">
Terms of Service
</Link>{" "}
apply.
<br />
By clicking "Register", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
<Trans>
This site is protected by ReCAPTCHA and the Google{" "}
<Link href="https://www.google.com/policies/privacy/">
Privacy Policy
</Link>{" "}
and{" "}
<Link href="https://policies.google.com/terms">
Terms of Service
</Link>{" "}
apply.
<br />
By clicking "Register", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
</Trans>
</Caption>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={registering}>
{registering ? "Registering..." : "Register"}
{registering ? t("Registering…") : t("Register")}
</Button>
</FieldRow>
<div id={recaptchaId} />
</form>
</div>
<div className={styles.authLinks}>
<p>Already have an account?</p>
<p>
<Link to="/login">Log in</Link>
{" Or "}
<Link to="/">Access as a guest</Link>
</p>
<Trans>
<p>Already have an account?</p>
<p>
<Link to="/login">Log in</Link>
{" Or "}
<Link to="/">Access as a guest</Link>
</p>
</Trans>
</div>
</div>
</div>

View File

@@ -29,7 +29,7 @@ export const useInteractiveLogin = () =>
password: string
) => Promise<[MatrixClient, Session]>
>(async (homeserver: string, username: string, password: string) => {
const authClient = createClient(homeserver);
const authClient = createClient({ baseUrl: homeserver });
const interactiveAuth = new InteractiveAuth({
matrixClient: authClient,

View File

@@ -37,7 +37,7 @@ export const useInteractiveRegistration = (): [
const authClient = useRef<MatrixClient>();
if (!authClient.current) {
authClient.current = createClient(defaultHomeserver);
authClient.current = createClient({ baseUrl: defaultHomeserver });
}
useEffect(() => {

View File

@@ -16,6 +16,9 @@ limitations under the License.
import { useEffect, useCallback, useRef, useState } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next";
import { translatedError } from "../TranslatedError";
declare global {
interface Window {
@@ -32,6 +35,7 @@ interface RecaptchaPromiseRef {
}
export const useRecaptcha = (sitekey: string) => {
const { t } = useTranslation();
const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef<RecaptchaPromiseRef>();
@@ -71,14 +75,14 @@ export const useRecaptcha = (sitekey: string) => {
if (!window.grecaptcha) {
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) => {
const observer = new MutationObserver((mutationsList) => {
for (const item of mutationsList) {
if ((item.target as HTMLElement)?.style?.visibility !== "visible") {
reject(new Error("Recaptcha dismissed"));
reject(translatedError("Recaptcha dismissed", t));
observer.disconnect();
return;
}
@@ -108,7 +112,7 @@ export const useRecaptcha = (sitekey: string) => {
});
}
});
}, [sitekey]);
}, [sitekey, t]);
const reset = useCallback(() => {
window.grecaptcha?.reset();

View File

@@ -18,6 +18,7 @@ import { PressEvent } from "@react-types/shared";
import classNames from "classnames";
import { useButton } from "@react-aria/button";
import { mergeProps, useObjectRef } from "@react-aria/utils";
import { useTranslation } from "react-i18next";
import styles from "./Button.module.css";
import { ReactComponent as MicIcon } from "../icons/Mic.svg";
@@ -142,9 +143,11 @@ export function MicButton({
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
const { t } = useTranslation();
return (
<TooltipTrigger
tooltip={() => (muted ? "Unmute microphone" : "Mute microphone")}
tooltip={() => (muted ? t("Unmute microphone") : t("Mute microphone"))}
>
<Button variant="toolbar" {...rest} off={muted}>
{muted ? <MuteMicIcon /> : <MicIcon />}
@@ -161,9 +164,11 @@ export function VideoButton({
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
const { t } = useTranslation();
return (
<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}>
{muted ? <DisableVideoIcon /> : <VideoIcon />}
@@ -182,9 +187,11 @@ export function ScreenshareButton({
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
const { t } = useTranslation();
return (
<TooltipTrigger
tooltip={() => (enabled ? "Stop sharing screen" : "Share screen")}
tooltip={() => (enabled ? t("Stop sharing screen") : t("Share screen"))}
>
<Button variant="toolbarSecondary" {...rest} on={enabled}>
<ScreenshareIcon />
@@ -201,8 +208,11 @@ export function HangupButton({
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Leave"), [t]);
return (
<TooltipTrigger tooltip={() => "Leave"}>
<TooltipTrigger tooltip={tooltip}>
<Button
variant="toolbar"
className={classNames(styles.hangupButton, className)}
@@ -222,8 +232,11 @@ export function SettingsButton({
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Settings"), [t]);
return (
<TooltipTrigger tooltip={() => "Settings"}>
<TooltipTrigger tooltip={tooltip}>
<Button variant="toolbar" {...rest}>
<SettingsIcon />
</Button>
@@ -239,8 +252,11 @@ export function InviteButton({
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Invite"), [t]);
return (
<TooltipTrigger tooltip={() => "Invite"}>
<TooltipTrigger tooltip={tooltip}>
<Button variant="toolbar" {...rest}>
<AddUserIcon />
</Button>
@@ -256,8 +272,11 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
}
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Local volume"), [t]);
return (
<TooltipTrigger tooltip={() => "Local volume"}>
<TooltipTrigger tooltip={tooltip}>
<Button variant="icon" {...rest}>
<VolumeIcon volume={volume} />
</Button>
@@ -273,12 +292,13 @@ export function FullscreenButton({
fullscreen,
...rest
}: FullscreenButtonProps) {
const getTooltip = useCallback(() => {
return fullscreen ? "Exit full screen" : "Full screen";
}, [fullscreen]);
const { t } = useTranslation();
const tooltip = useCallback(() => {
return fullscreen ? t("Exit full screen") : t("Full screen");
}, [fullscreen, t]);
return (
<TooltipTrigger tooltip={getTooltip}>
<TooltipTrigger tooltip={tooltip}>
<Button variant="icon" {...rest}>
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
</Button>

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React from "react";
import { useTranslation } from "react-i18next";
import useClipboard from "react-use-clipboard";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
@@ -36,6 +37,7 @@ export function CopyButton({
copiedMessage,
...rest
}: Props) {
const { t } = useTranslation();
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
return (
@@ -49,7 +51,7 @@ export function CopyButton({
>
{isCopied ? (
<>
{variant !== "icon" && <span>{copiedMessage || "Copied!"}</span>}
{variant !== "icon" && <span>{copiedMessage || t("Copied!")}</span>}
<CheckIcon />
</>
) : (

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React, { FC } from "react";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import { Headline } from "../typography/Typography";
import { Button } from "../button";
@@ -39,25 +40,29 @@ interface Props {
}
export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
const { t } = useTranslation();
return (
<PopoverMenuTrigger placement="bottom">
<Button variant="dropdown" className={commonStyles.headline}>
<Headline className={styles.label}>
{callType === CallType.Video ? "Video call" : "Walkie-talkie call"}
{callType === CallType.Video
? t("Video call")
: t("Walkie-talkie call")}
</Headline>
</Button>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="Call type menu" onAction={setCallType}>
<Item key={CallType.Video} textValue="Video call">
<Menu {...props} label={t("Call type menu")} onAction={setCallType}>
<Item key={CallType.Video} textValue={t("Video call")}>
<VideoIcon />
<span>Video call</span>
<span>{t("Video call")}</span>
{callType === CallType.Video && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
<Item key={CallType.Radio} textValue="Walkie-talkie call">
<Item key={CallType.Radio} textValue={t("Walkie-talkie call")}>
<MicIcon />
<span>Walkie-talkie call</span>
<span>{t("Walkie-talkie call")}</span>
{callType === CallType.Radio && (
<CheckIcon className={menuStyles.checkIcon} />
)}

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React from "react";
import { useTranslation } from "react-i18next";
import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
@@ -23,7 +24,8 @@ import { RegisteredView } from "./RegisteredView";
import { usePageTitle } from "../usePageTitle";
export function HomePage() {
usePageTitle("Home");
const { t } = useTranslation();
usePageTitle(t("Home"));
const { isAuthenticated, isPasswordlessUser, loading, error, client } =
useClient();

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import { PressEvent } from "@react-types/shared";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent } from "../Modal";
import { Button } from "../button";
@@ -29,13 +30,15 @@ interface Props {
[index: string]: unknown;
}
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
const { t } = useTranslation();
return (
<Modal title="Join existing call?" isDismissable {...rest}>
<Modal title={t("Join existing call?")} isDismissable {...rest}>
<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}>
<Button onPress={onClose}>No</Button>
<Button onPress={onJoin}>Yes, join call</Button>
<Button onPress={onClose}>{t("No")}</Button>
<Button onPress={onJoin}>{t("Yes, join call")}</Button>
</FieldRow>
</ModalContent>
</Modal>

View File

@@ -22,6 +22,7 @@ import React, {
} from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
import { useGroupCallRooms } from "./useGroupCallRooms";
@@ -48,6 +49,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const history = useHistory();
const { t } = useTranslation();
const { modalState, modalProps } = useModalTriggerState();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
@@ -93,7 +95,9 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
}, [history, existingRoomId]);
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 (
<>
@@ -127,19 +131,19 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) {
className={styles.button}
disabled={loading}
>
{loading ? "Loading..." : "Go"}
{loading ? t("Loading…") : t("Go")}
</Button>
</FieldRow>
{error && (
<FieldRow className={styles.fieldRow}>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
</Form>
{recentRooms.length > 0 && (
<>
<Title className={styles.recentCallsTitle}>
Your recent Calls
{t("Your recent calls")}
</Title>
<CallList rooms={recentRooms} client={client} disableFacepile />
</>

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React, { FC, useCallback, useState, FormEventHandler } from "react";
import { useHistory } from "react-router-dom";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { Trans, useTranslation } from "react-i18next";
import { useClient } from "../ClientContext";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
@@ -47,6 +48,7 @@ export const UnauthenticatedView: FC = () => {
const { modalState, modalProps } = useModalTriggerState();
const [onFinished, setOnFinished] = useState<() => void>();
const history = useHistory();
const { t } = useTranslation();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e) => {
@@ -105,7 +107,9 @@ export const UnauthenticatedView: FC = () => {
);
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 (
<>
@@ -137,24 +141,26 @@ export const UnauthenticatedView: FC = () => {
<InputField
id="displayName"
name="displayName"
label="Display Name"
placeholder="Display Name"
label={t("Display name")}
placeholder={t("Display name")}
type="text"
required
autoComplete="off"
/>
</FieldRow>
<Caption>
By clicking "Go", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
<Trans>
By clicking "Go", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
</Trans>
</Caption>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
<Button type="submit" size="lg" disabled={loading}>
{loading ? "Loading..." : "Go"}
{loading ? t("Loading…") : t("Go")}
</Button>
<div id={recaptchaId} />
</Form>
@@ -162,14 +168,16 @@ export const UnauthenticatedView: FC = () => {
<footer className={styles.footer}>
<Body className={styles.mobileLoginLink}>
<Link color="primary" to="/login">
Login to your account
{t("Login to your account")}
</Link>
</Body>
<Body>
Not registered yet?{" "}
<Link color="primary" to="/register">
Create an account
</Link>
<Trans>
Not registered yet?{" "}
<Link color="primary" to="/register">
Create an account
</Link>
</Trans>
</Body>
</footer>
</div>

View File

@@ -20,6 +20,7 @@ import { useCallback } from "react";
import { useState } from "react";
import { forwardRef } from "react";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { Avatar, Size } from "../Avatar";
import { Button } from "../button";
@@ -39,6 +40,8 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
ref
) => {
const { t } = useTranslation();
const [removed, setRemoved] = useState(false);
const [objUrl, setObjUrl] = useState<string>(null);
@@ -97,7 +100,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
variant="icon"
onPress={onPressRemoveAvatar}
>
Remove
{t("Remove")}
</Button>
)}
</div>

View File

@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent, forwardRef, ReactNode } from "react";
import React, { ChangeEvent, FC, forwardRef, ReactNode } from "react";
import classNames from "classnames";
import styles from "./Input.module.css";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import { TranslatedError } from "../TranslatedError";
interface FieldRowProps {
children: ReactNode;
@@ -140,10 +141,12 @@ export const InputField = forwardRef<
}
);
export function ErrorMessage({
children,
}: {
children: ReactNode;
}): JSX.Element {
return <p className={styles.errorMessage}>{children}</p>;
interface ErrorMessageProps {
error: Error;
}
export const ErrorMessage: FC<ErrorMessageProps> = ({ error }) => (
<p className={styles.errorMessage}>
{error instanceof TranslatedError ? error.translatedMessage : error.message}
</p>
);

View File

@@ -19,6 +19,7 @@ import { AriaSelectOptions, HiddenSelect, useSelect } from "@react-aria/select";
import { useButton } from "@react-aria/button";
import { useSelectState } from "@react-stately/select";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { Popover } from "../popover/Popover";
import { ListBox } from "../ListBox";
@@ -30,6 +31,7 @@ interface Props extends AriaSelectOptions<object> {
}
export function SelectInput(props: Props): JSX.Element {
const { t } = useTranslation();
const state = useSelectState(props);
const ref = useRef();
@@ -56,7 +58,7 @@ export function SelectInput(props: Props): JSX.Element {
<span {...valueProps} className={styles.selectedItem}>
{state.selectedItem
? state.selectedItem.rendered
: "Select an option"}
: t("Select an option")}
</span>
<ArrowDownIcon />
</button>

View File

@@ -25,10 +25,15 @@ import ReactDOM from "react-dom";
import { createBrowserHistory } from "history";
import * as Sentry from "@sentry/react";
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 App from "./App";
import { init as initRageshake } from "./settings/rageshake";
import { getUrlParams } from "./UrlParams";
initRageshake();
@@ -104,6 +109,35 @@ Sentry.init({
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(
<React.StrictMode>
<App history={history} />

View File

@@ -19,7 +19,7 @@ import {
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room } from "matrix-js-sdk/src/models/room";
import IndexedDBWorker from "./IndexedDBWorker?worker";
import { getRoomParams } from "./room/useRoomParams";
import { getUrlParams } from "./UrlParams";
export const defaultHomeserver =
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
@@ -134,12 +134,12 @@ export async function initClient(
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
// the values around, but we initialise the matrix client in
// many different places so we'd have to pass it into all of
// them.
const { e2eEnabled } = getRoomParams();
const { e2eEnabled } = getUrlParams();
if (!e2eEnabled) {
logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
}

71
src/media-utils.ts Normal file
View 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();
}
}
}

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React, { ChangeEvent, useCallback, useEffect, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { useProfile } from "./useProfile";
@@ -31,6 +32,7 @@ interface Props {
}
export function ProfileModal({ client, ...rest }: Props) {
const { onClose } = rest;
const { t } = useTranslation();
const {
success,
error,
@@ -83,14 +85,14 @@ export function ProfileModal({ client, ...rest }: Props) {
}, [success, onClose]);
return (
<Modal title="Profile" isDismissable {...rest}>
<Modal title={t("Profile")} isDismissable {...rest}>
<ModalContent>
<form onSubmit={onSubmit}>
<FieldRow className={styles.avatarFieldRow}>
<AvatarInputField
id="avatar"
name="avatar"
label="Avatar"
label={t("Avatar")}
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
@@ -100,7 +102,7 @@ export function ProfileModal({ client, ...rest }: Props) {
<InputField
id="userId"
name="userId"
label="User Id"
label={t("User ID")}
type="text"
disabled
value={client.getUserId()}
@@ -110,18 +112,18 @@ export function ProfileModal({ client, ...rest }: Props) {
<InputField
id="displayName"
name="displayName"
label="Display Name"
label={t("Display name")}
type="text"
required
autoComplete="off"
placeholder="Display Name"
placeholder={t("Display name")}
value={displayName}
onChange={onChangeDisplayName}
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
<FieldRow rightAlign>
@@ -129,7 +131,7 @@ export function ProfileModal({ client, ...rest }: Props) {
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading ? "Saving..." : "Save"}
{loading ? t("Saving…") : t("Save")}
</Button>
</FieldRow>
</form>

View File

@@ -101,7 +101,9 @@ export function useProfile(client: MatrixClient) {
if (removeAvatar) {
await client.setAvatarUrl("");
} else if (avatar) {
mxcAvatarUrl = await client.uploadContent(avatar);
({ content_uri: mxcAvatarUrl } = await client.uploadContent(
avatar
));
await client.setAvatarUrl(mxcAvatarUrl);
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React from "react";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import styles from "./AudioPreview.module.css";
import { SelectInput } from "../input/SelectInput";
@@ -43,24 +44,26 @@ export function AudioPreview({
audioOutputs,
setAudioOutput,
}: Props) {
const { t } = useTranslation();
return (
<>
<h1>{`${roomName} - Walkie-talkie call`}</h1>
<h1>{t("{{roomName}} - Walkie-talkie call", { roomName })}</h1>
<div className={styles.preview}>
{state === GroupCallState.LocalCallFeedUninitialized && (
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
Microphone permissions needed to join the call.
{t("Microphone permissions needed to join the call.")}
</Body>
)}
{state === GroupCallState.InitializingLocalCallFeed && (
<Body fontWeight="semiBold" className={styles.microphonePermissions}>
Accept microphone permissions to join the call.
{t("Accept microphone permissions to join the call.")}
</Body>
)}
{state === GroupCallState.LocalCallFeedInitialized && (
<>
<SelectInput
label="Microphone"
label={t("Microphone")}
selectedKey={audioInput}
onSelectionChange={setAudioInput}
className={styles.inputField}
@@ -69,13 +72,13 @@ export function AudioPreview({
<Item key={deviceId}>
{!!label && label.trim().length > 0
? label
: `Microphone ${index + 1}`}
: t("Microphone {{n}}", { n: index + 1 })}
</Item>
))}
</SelectInput>
{audioOutputs.length > 0 && (
<SelectInput
label="Speaker"
label={t("Speaker")}
selectedKey={audioOutput}
onSelectionChange={setAudioOutput}
className={styles.inputField}
@@ -84,7 +87,7 @@ export function AudioPreview({
<Item key={deviceId}>
{!!label && label.trim().length > 0
? label
: `Speaker ${index + 1}`}
: t("Speaker {{n}}", { n: index + 1 })}
</Item>
))}
</SelectInput>

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Trans, useTranslation } from "react-i18next";
import styles from "./CallEndedView.module.css";
import { LinkButton } from "../button";
@@ -24,6 +25,7 @@ import { Subtitle, Body, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
export function CallEndedView({ client }: { client: MatrixClient }) {
const { t } = useTranslation();
const { displayName } = useProfile(client);
return (
@@ -37,29 +39,31 @@ export function CallEndedView({ client }: { client: MatrixClient }) {
<div className={styles.container}>
<main className={styles.main}>
<Headline className={styles.headline}>
{displayName}, your call is now ended
{t("{{displayName}}, your call is now ended", { displayName })}
</Headline>
<div className={styles.callEndedContent}>
<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
future calls
</Subtitle>
<Trans>
<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
future calls
</Subtitle>
</Trans>
<LinkButton
className={styles.callEndedButton}
size="lg"
variant="default"
to="/register"
>
Create account
{t("Create account")}
</LinkButton>
</div>
</main>
<Body className={styles.footer}>
<Link color="primary" to="/">
Not now, return to home screen
{t("Not now, return to home screen")}
</Link>
</Body>
</div>

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React, { useCallback, useEffect } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent } from "../Modal";
import { Button } from "../button";
@@ -25,6 +26,7 @@ import {
useRageshakeRequest,
} from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
interface Props {
inCall: boolean;
roomId: string;
@@ -32,7 +34,9 @@ interface Props {
// TODO: add all props for for <Modal>
[index: string]: unknown;
}
export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendRageshakeRequest = useRageshakeRequest();
@@ -67,15 +71,20 @@ export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
}, [sent, onClose]);
return (
<Modal title="Submit Feedback" isDismissable onClose={onClose} {...rest}>
<Modal
title={t("Submit feedback")}
isDismissable
onClose={onClose}
{...rest}
>
<ModalContent>
<Body>Having trouble? Help us fix it.</Body>
<Body>{t("Having trouble? Help us fix it.")}</Body>
<form onSubmit={onSubmitFeedback}>
<FieldRow>
<InputField
id="description"
name="description"
label="Description (optional)"
label={t("Description (optional)")}
type="textarea"
/>
</FieldRow>
@@ -83,19 +92,19 @@ export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
<InputField
id="sendLogs"
name="sendLogs"
label="Include Debug Logs"
label={t("Include debug logs")}
type="checkbox"
defaultChecked
/>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
<FieldRow>
<Button type="submit" disabled={sending}>
{sending ? "Submitting feedback..." : "Submit Feedback"}
{sending ? t("Submitting feedback…") : t("Submit feedback")}
</Button>
</FieldRow>
</form>

View File

@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { useCallback } from "react";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
@@ -27,28 +28,33 @@ import { Menu } from "../Menu";
import { TooltipTrigger } from "../Tooltip";
export type Layout = "freedom" | "spotlight";
interface Props {
layout: Layout;
setLayout: (layout: Layout) => void;
}
export function GridLayoutMenu({ layout, setLayout }: Props) {
const { t } = useTranslation();
const tooltip = useCallback(() => t("Change layout"), [t]);
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={() => "Layout Type"}>
<TooltipTrigger tooltip={tooltip}>
<Button variant="icon">
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
<Item key="freedom" textValue="Freedom">
<Menu {...props} label={t("Grid layout menu")} onAction={setLayout}>
<Item key="freedom" textValue={t("Freedom")}>
<FreedomIcon />
<span>Freedom</span>
{layout === "freedom" && (
<CheckIcon className={menuStyles.checkIcon} />
)}
</Item>
<Item key="spotlight" textValue="Spotlight">
<Item key="spotlight" textValue={t("Spotlight")}>
<SpotlightIcon />
<span>Spotlight</span>
{layout === "spotlight" && (

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React, { ReactNode } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useTranslation } from "react-i18next";
import { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
@@ -37,6 +38,7 @@ export function GroupCallLoader({
children,
createPtt,
}: Props): JSX.Element {
const { t } = useTranslation();
const { loading, error, groupCall } = useLoadGroupCall(
client,
roomIdOrAlias,
@@ -44,12 +46,12 @@ export function GroupCallLoader({
createPtt
);
usePageTitle(groupCall ? groupCall.room.name : "Loading...");
usePageTitle(groupCall ? groupCall.room.name : t("Loading…"));
if (loading) {
return (
<FullScreenView>
<h1>Loading room...</h1>
<h1>{t("Loading room…")}</h1>
</FullScreenView>
);
}

View File

@@ -18,6 +18,8 @@ import React, { useCallback, useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
@@ -31,6 +33,7 @@ import { useRoomAvatar } from "./useRoomAvatar";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { useLocationNavigation } from "../useLocationNavigation";
import { useMediaHandler } from "../settings/useMediaHandler";
import { findDeviceByName, getDevices } from "../media-utils";
declare global {
interface Window {
@@ -79,8 +82,8 @@ export function GroupCallView({
unencryptedEventsFromUsers,
} = useGroupCall(groupCall);
const { t } = useTranslation();
const { setAudioInput, setVideoInput } = useMediaHandler();
const avatarUrl = useRoomAvatar(groupCall.room);
useEffect(() => {
@@ -94,10 +97,45 @@ export function GroupCallView({
if (widget && preload) {
// In preload mode, wait for a join action before entering
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
.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([
groupCall.setMicrophoneMuted(audioInput === null),
groupCall.setLocalVideoMuted(videoInput === null),
@@ -203,7 +241,7 @@ export function GroupCallView({
} else if (state === GroupCallState.Entering) {
return (
<FullScreenView>
<h1>Entering room...</h1>
<h1>{t("Entering room…")}</h1>
</FullScreenView>
);
} else if (left) {
@@ -220,7 +258,7 @@ export function GroupCallView({
} else if (isEmbedded) {
return (
<FullScreenView>
<h1>Loading room...</h1>
<h1>{t("Loading room…")}</h1>
</FullScreenView>
);
} else {

View File

@@ -23,6 +23,8 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
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 styles from "./InCallView.module.css";
@@ -56,6 +58,8 @@ import { useFullscreen } from "../video-grid/useFullscreen";
import { AudioContainer } from "../video-grid/AudioContainer";
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
import { widget, ElementWidgetActions } from "../widget";
import { useJoinRule } from "./useJoinRule";
import { useUrlParams } from "../UrlParams";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -112,7 +116,10 @@ export function InCallView({
unencryptedEventsFromUsers,
hideHeader,
}: Props) {
const { t } = useTranslation();
usePreventScroll();
const joinRule = useJoinRule(groupCall.room);
const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver });
// Merge the refs so they can attach to the same element
@@ -139,6 +146,8 @@ export function InCallView({
useAudioOutputDevice(audioRef, audioOutput);
const { hideScreensharing } = useUrlParams();
useEffect(() => {
widget?.api.transport.send(
layout === "freedom"
@@ -215,14 +224,17 @@ export function InCallView({
// window is too small to show everyone
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.callFeed) ??
null
: null,
: null),
[fullscreenParticipant, bounds, items]
);
const reducedControls = bounds.width <= 400;
const renderAvatar = useCallback(
(roomMember: RoomMember, width: number, height: number) => {
const avatarUrl = roomMember.user?.avatarUrl;
@@ -245,7 +257,7 @@ export function InCallView({
if (items.length === 0) {
return (
<div className={styles.centerMessage}>
<p>Waiting for other participants...</p>
<p>{t("Waiting for other participants…")}</p>
</div>
);
}
@@ -325,18 +337,21 @@ export function InCallView({
<div className={styles.footer}>
<MicButton muted={microphoneMuted} onPress={toggleMicrophoneMuted} />
<VideoButton muted={localVideoMuted} onPress={toggleLocalVideoMuted} />
{canScreenshare && !isSafari && !maximisedParticipant && (
<ScreenshareButton
enabled={isScreensharing}
onPress={toggleScreensharing}
/>
)}
{!maximisedParticipant && (
{canScreenshare &&
!hideScreensharing &&
!isSafari &&
!reducedControls && (
<ScreenshareButton
enabled={isScreensharing}
onPress={toggleScreensharing}
/>
)}
{!reducedControls && (
<OverflowMenu
inCall
roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall}
showInvite={true}
showInvite={joinRule === JoinRule.Public}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
/>

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { CopyButton } from "../button";
@@ -25,19 +26,23 @@ interface Props extends Omit<ModalProps, "title" | "children"> {
roomIdOrAlias: string;
}
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => (
<Modal
title="Invite People"
isDismissable
className={styles.inviteModal}
{...rest}
>
<ModalContent>
<p>Copy and share this meeting link</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
/>
</ModalContent>
</Modal>
);
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
const { t } = useTranslation();
return (
<Modal
title={t("Invite people")}
isDismissable
className={styles.inviteModal}
{...rest}
>
<ModalContent>
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
/>
</ModalContent>
</Modal>
);
};

View File

@@ -19,6 +19,7 @@ import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { PressEvent } from "@react-types/shared";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { useTranslation } from "react-i18next";
import styles from "./LobbyView.module.css";
import { Button, CopyButton } from "../button";
@@ -66,6 +67,7 @@ export function LobbyView({
isEmbedded,
hideHeader,
}: Props) {
const { t } = useTranslation();
const { stream } = useCallFeed(localCallFeed);
const {
audioInput,
@@ -142,15 +144,15 @@ export function LobbyView({
variant="secondaryCopy"
value={getRoomUrl(roomIdOrAlias)}
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>
</div>
{!isEmbedded && (
<Body className={styles.joinRoomFooter}>
<Link color="primary" to="/">
Take me Home
{t("Take me Home")}
</Link>
</Body>
)}

View File

@@ -18,6 +18,7 @@ import React, { useCallback } from "react";
import { Item } from "@react-stately/collections";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { OverlayTriggerState } from "@react-stately/overlays";
import { useTranslation } from "react-i18next";
import { Button } from "../button";
import { Menu } from "../Menu";
@@ -31,6 +32,7 @@ import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal";
interface Props {
roomIdOrAlias: string;
inCall: boolean;
@@ -42,6 +44,7 @@ interface Props {
onClose: () => void;
};
}
export function OverflowMenu({
roomIdOrAlias,
inCall,
@@ -50,6 +53,8 @@ export function OverflowMenu({
feedbackModalState,
feedbackModalProps,
}: Props) {
const { t } = useTranslation();
const {
modalState: inviteModalState,
modalProps: inviteModalProps,
@@ -90,29 +95,31 @@ export function OverflowMenu({
[feedbackModalState, inviteModalState, settingsModalState]
);
const tooltip = useCallback(() => t("More"), [t]);
return (
<>
<PopoverMenuTrigger disableOnState>
<TooltipTrigger tooltip={() => "More"} placement="top">
<TooltipTrigger tooltip={tooltip} placement="top">
<Button variant="toolbar">
<OverflowIcon />
</Button>
</TooltipTrigger>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="more menu" onAction={onAction}>
<Menu {...props} label={t("More menu")} onAction={onAction}>
{showInvite && (
<Item key="invite" textValue="Invite people">
<Item key="invite" textValue={t("Invite people")}>
<AddUserIcon />
<span>Invite people</span>
<span>{t("Invite people")}</span>
</Item>
)}
<Item key="settings" textValue="Settings">
<Item key="settings" textValue={t("Settings")}>
<SettingsIcon />
<span>Settings</span>
<span>{t("Settings")}</span>
</Item>
<Item key="feedback" textValue="Submit Feedback">
<Item key="feedback" textValue={t("Submit feedback")}>
<FeedbackIcon />
<span>Submit Feedback</span>
<span>{t("Submit feedback")}</span>
</Item>
</Menu>
)}

View File

@@ -17,10 +17,12 @@ limitations under the License.
import React, { useEffect } from "react";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import i18n from "i18next";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { useTranslation } from "react-i18next";
import { useDelayedState } from "../useDelayedState";
import { useModalTriggerState } from "../Modal";
@@ -50,40 +52,45 @@ function getPromptText(
talkOverEnabled: boolean,
activeSpeakerUserId: string,
activeSpeakerDisplayName: string,
connected: boolean
connected: boolean,
t: typeof i18n.t
): string {
if (!connected) return "Connection lost";
if (!connected) return t("Connection lost");
const isTouchScreen = Boolean(window.ontouchstart !== undefined);
if (networkWaiting) {
return "Waiting for network";
return t("Waiting for network");
}
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 (isTouchScreen) {
return "Release to stop";
return t("Release to stop");
} else {
return "Release spacebar key to stop";
return t("Release spacebar key to stop");
}
}
if (talkOverEnabled && activeSpeakerUserId && !activeSpeakerIsLocalUser) {
if (isTouchScreen) {
return `Press and hold to talk over ${activeSpeakerDisplayName}`;
return t("Press and hold to talk over {{name}}", {
name: activeSpeakerDisplayName,
});
} else {
return `Press and hold spacebar to talk over ${activeSpeakerDisplayName}`;
return t("Press and hold spacebar to talk over {{name}}", {
name: activeSpeakerDisplayName,
});
}
}
if (isTouchScreen) {
return "Press and hold to talk";
return t("Press and hold to talk");
} 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,
hideHeader,
}) => {
const { t } = useTranslation();
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
@@ -195,9 +203,11 @@ export const PTTCallView: React.FC<Props> = ({
{showControls && (
<>
<div className={styles.participants}>
<p>{`${participants.length} ${
participants.length > 1 ? "people" : "person"
} connected`}</p>
<p>
{t("{{count}} people connected", {
count: participants.length,
})}
</p>
<Facepile
size={facepileSize}
max={8}
@@ -230,8 +240,10 @@ export const PTTCallView: React.FC<Props> = ({
<AudioIcon className={styles.speakerIcon} />
)}
{activeSpeakerIsLocalUser
? "Talking..."
: `${activeSpeakerDisplayName} is talking...`}
? t("Talking…")
: t("{{name}} is talking…", {
name: activeSpeakerDisplayName,
})}
</h2>
<Timer value={activeSpeakerUserId} />
</div>
@@ -263,7 +275,8 @@ export const PTTCallView: React.FC<Props> = ({
talkOverEnabled,
activeSpeakerUserId,
activeSpeakerDisplayName,
connected
connected,
t
)}
</p>
)}
@@ -278,7 +291,7 @@ export const PTTCallView: React.FC<Props> = ({
<Toggle
isSelected={talkOverEnabled}
onChange={setTalkOverEnabled}
label="Talk over speaker"
label={t("Talk over speaker")}
id="talkOverEnabled"
/>
)}

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { Button } from "../button";
@@ -33,6 +34,7 @@ export const RageshakeRequestModal: FC<Props> = ({
roomIdOrAlias,
...rest
}) => {
const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
@@ -42,11 +44,12 @@ export const RageshakeRequestModal: FC<Props> = ({
}, [sent, rest]);
return (
<Modal title="Debug Log Request" isDismissable {...rest}>
<Modal title={t("Debug log request")} isDismissable {...rest}>
<ModalContent>
<Body>
Another user on this call is having an issue. In order to better
diagnose these issues we'd like to collect a debug log.
{t(
"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>
<FieldRow>
<Button
@@ -59,12 +62,12 @@ export const RageshakeRequestModal: FC<Props> = ({
}
disabled={sending}
>
{sending ? "Sending debug log..." : "Send debug log"}
{sending ? t("Sending debug logs…") : t("Send debug logs")}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
</ModalContent>

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React, { useCallback, useState } from "react";
import { useLocation } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import styles from "./RoomAuthView.module.css";
import { Button } from "../button";
@@ -50,6 +51,7 @@ export function RoomAuthView() {
[registerPasswordlessUser]
);
const { t } = useTranslation();
const location = useLocation();
return (
@@ -64,42 +66,46 @@ export function RoomAuthView() {
</Header>
<div className={styles.container}>
<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}>
<FieldRow>
<InputField
id="displayName"
name="displayName"
label="Display Name"
placeholder="Display Name"
label={t("Display name")}
placeholder={t("Display name")}
type="text"
required
autoComplete="off"
/>
</FieldRow>
<Caption>
By clicking "Join call now", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
<Trans>
By clicking "Join call now", you agree to our{" "}
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
</Trans>
</Caption>
{error && (
<FieldRow>
<ErrorMessage>{error.message}</ErrorMessage>
<ErrorMessage error={error} />
</FieldRow>
)}
<Button type="submit" size="lg" disabled={loading}>
{loading ? "Loading..." : "Join call now"}
{loading ? t("Loading…") : t("Join call now")}
</Button>
<div id={recaptchaId} />
</Form>
</main>
<Body className={styles.footer}>
{"Not registered yet? "}
<Link
color="primary"
to={{ pathname: "/login", state: { from: location } }}
>
Create an account
</Link>
<Trans>
Not registered yet?{" "}
<Link
color="primary"
to={{ pathname: "/login", state: { from: location } }}
>
Create an account
</Link>
</Trans>
</Body>
</div>
</>

View File

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { FC, useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useClient } from "../ClientContext";
@@ -22,11 +23,13 @@ import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView";
import { useRoomParams } from "./useRoomParams";
import { useUrlParams } from "../UrlParams";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { translatedError } from "../TranslatedError";
export const RoomPage: FC = () => {
const { t } = useTranslation();
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
@@ -39,9 +42,9 @@ export const RoomPage: FC = () => {
hideHeader,
isPtt,
displayName,
} = useRoomParams();
} = useUrlParams();
const roomIdOrAlias = roomId ?? roomAlias;
if (!roomIdOrAlias) throw new Error("No room specified");
if (!roomIdOrAlias) throw translatedError("No room specified", t);
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
const [isRegistering, setIsRegistering] = useState(false);

View File

@@ -19,6 +19,7 @@ import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { MicButton, VideoButton } from "../button";
import { useMediaStream } from "../video-grid/useMediaStream";
@@ -40,6 +41,7 @@ interface Props {
audioOutput: string;
stream: MediaStream;
}
export function VideoPreview({
client,
state,
@@ -51,6 +53,7 @@ export function VideoPreview({
audioOutput,
stream,
}: Props) {
const { t } = useTranslation();
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
@@ -64,12 +67,12 @@ export function VideoPreview({
<video ref={videoRef} muted playsInline disablePictureInPicture />
{state === GroupCallState.LocalCallFeedUninitialized && (
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
Camera/microphone permissions needed to join the call.
{t("Camera/microphone permissions needed to join the call.")}
</Body>
)}
{state === GroupCallState.InitializingLocalCallFeed && (
<Body fontWeight="semiBold" className={styles.cameraPermissions}>
Accept camera/microphone permissions to join the call.
{t("Accept camera/microphone permissions to join the call.")}
</Body>
)}
{state === GroupCallState.LocalCallFeedInitialized && (

View File

@@ -26,8 +26,10 @@ import {
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useTranslation } from "react-i18next";
import { usePageUnload } from "./usePageUnload";
import { TranslatedError, translatedError } from "../TranslatedError";
export interface UseGroupCallReturnType {
state: GroupCallState;
@@ -37,7 +39,7 @@ export interface UseGroupCallReturnType {
userMediaFeeds: CallFeed[];
microphoneMuted: boolean;
localVideoMuted: boolean;
error: Error;
error: TranslatedError | null;
initLocalCallFeed: () => void;
enter: () => void;
leave: () => void;
@@ -60,7 +62,7 @@ interface State {
localCallFeed: CallFeed;
activeSpeaker: string;
userMediaFeeds: CallFeed[];
error: Error;
error: TranslatedError | null;
microphoneMuted: boolean;
localVideoMuted: boolean;
screenshareFeeds: CallFeed[];
@@ -309,15 +311,18 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
});
}, [groupCall]);
const { t } = useTranslation();
useEffect(() => {
if (window.RTCPeerConnection === undefined) {
const error = new Error(
"WebRTC is not supported or is being blocked in this browser."
const error = translatedError(
"WebRTC is not supported or is being blocked in this browser.",
t
);
console.error(error);
updateState({ error });
}
}, []);
}, [t]);
return {
state,

26
src/room/useJoinRule.ts Normal file
View 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(), [])
);

View File

@@ -24,10 +24,12 @@ import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEv
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
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 { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
import { translatedError } from "../TranslatedError";
export interface GroupCallLoadState {
loading: boolean;
@@ -41,6 +43,7 @@ export const useLoadGroupCall = (
viaServers: string[],
createPtt: boolean
): GroupCallLoadState => {
const { t } = useTranslation();
const [state, setState] = useState<GroupCallLoadState>({ loading: true });
useEffect(() => {
@@ -122,7 +125,7 @@ export const useLoadGroupCall = (
const timeout = setTimeout(() => {
client.off(GroupCallEventHandlerEvent.Incoming, onGroupCallIncoming);
reject(new Error("Fetching group call timed out."));
reject(translatedError("Fetching group call timed out.", t));
}, 30000);
});
};
@@ -153,7 +156,7 @@ export const useLoadGroupCall = (
.catch((error) =>
setState((prevState) => ({ ...prevState, loading: false, error }))
);
}, [client, roomIdOrAlias, viaServers, createPtt]);
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
return state;
};

View File

@@ -34,7 +34,7 @@ function isIOS() {
export function usePageUnload(callback: () => void) {
useEffect(() => {
let pageVisibilityTimeout: number;
let pageVisibilityTimeout: ReturnType<typeof setTimeout>;
function onBeforeUnload(event: PageTransitionEvent) {
if (event.type === "visibilitychange") {

View File

@@ -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 { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { EventType } from "matrix-js-sdk/src/@types/event";
export const useRoomAvatar = (room: Room) => {
const [avatarUrl, setAvatarUrl] = useState(room.getMxcAvatarUrl());
import { useRoomState } from "./useRoomState";
useEffect(() => {
const update = (ev: MatrixEvent) => {
if (ev.getType() === EventType.RoomAvatar) {
setAvatarUrl(room.getMxcAvatarUrl());
}
};
room.currentState.on(RoomStateEvent.Events, update);
return () => {
room.currentState.off(RoomStateEvent.Events, update);
};
}, [room]);
return avatarUrl;
};
export const useRoomAvatar = (room: Room) =>
useRoomState(
room,
useCallback(() => room.getMxcAvatarUrl(), [room])
);

39
src/room/useRoomState.ts Normal file
View 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]);
};

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React from "react";
import { Item } from "@react-stately/collections";
import { useTranslation } from "react-i18next";
import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css";
@@ -37,6 +38,7 @@ interface Props {
}
export const SettingsModal = (props: Props) => {
const { t } = useTranslation();
const {
audioInput,
audioInputs,
@@ -56,7 +58,7 @@ export const SettingsModal = (props: Props) => {
return (
<Modal
title="Settings"
title={t("Settings")}
isDismissable
mobileFullScreen
className={styles.settingsModal}
@@ -67,12 +69,12 @@ export const SettingsModal = (props: Props) => {
title={
<>
<AudioIcon width={16} height={16} />
<span>Audio</span>
<span>{t("Audio")}</span>
</>
}
>
<SelectInput
label="Microphone"
label={t("Microphone")}
selectedKey={audioInput}
onSelectionChange={setAudioInput}
>
@@ -80,13 +82,13 @@ export const SettingsModal = (props: Props) => {
<Item key={deviceId}>
{!!label && label.trim().length > 0
? label
: `Microphone ${index + 1}`}
: t("Microphone {{n}}", { n: index + 1 })}
</Item>
))}
</SelectInput>
{audioOutputs.length > 0 && (
<SelectInput
label="Speaker"
label={t("Speaker")}
selectedKey={audioOutput}
onSelectionChange={setAudioOutput}
>
@@ -94,7 +96,7 @@ export const SettingsModal = (props: Props) => {
<Item key={deviceId}>
{!!label && label.trim().length > 0
? label
: `Speaker ${index + 1}`}
: t("Speaker {{n}}", { n: index + 1 })}
</Item>
))}
</SelectInput>
@@ -102,10 +104,12 @@ export const SettingsModal = (props: Props) => {
<FieldRow>
<InputField
id="spatialAudio"
label="Spatial audio"
label={t("Spatial audio")}
type="checkbox"
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>) =>
setSpatialAudio(event.target.checked)
}
@@ -116,12 +120,12 @@ export const SettingsModal = (props: Props) => {
title={
<>
<VideoIcon width={16} height={16} />
<span>Video</span>
<span>{t("Video")}</span>
</>
}
>
<SelectInput
label="Camera"
label={t("Camera")}
selectedKey={videoInput}
onSelectionChange={setVideoInput}
>
@@ -129,7 +133,7 @@ export const SettingsModal = (props: Props) => {
<Item key={deviceId}>
{!!label && label.trim().length > 0
? label
: `Camera ${index + 1}`}
: t("Camera {{n}}", { n: index + 1 })}
</Item>
))}
</SelectInput>
@@ -138,20 +142,22 @@ export const SettingsModal = (props: Props) => {
title={
<>
<DeveloperIcon width={16} height={16} />
<span>Developer</span>
<span>{t("Developer")}</span>
</>
}
>
<FieldRow>
<Body className={styles.fieldRowText}>
Version: {import.meta.env.VITE_APP_VERSION || "dev"}
{t("Version: {{version}}", {
version: import.meta.env.VITE_APP_VERSION || "dev",
})}
</Body>
</FieldRow>
<FieldRow>
<InputField
id="showInspector"
name="inspector"
label="Show Call Inspector"
label={t("Show call inspector")}
type="checkbox"
checked={showInspector}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
@@ -160,7 +166,9 @@ export const SettingsModal = (props: Props) => {
/>
</FieldRow>
<FieldRow>
<Button onPress={downloadDebugLog}>Download Debug Logs</Button>
<Button onPress={downloadDebugLog}>
{t("Download debug logs")}
</Button>
</FieldRow>
</TabItem>
</TabContainer>

View File

@@ -16,6 +16,12 @@ limitations under the License.
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
export const useEventTarget = <T extends Event>(
target: EventTarget,
@@ -31,4 +37,20 @@ export const useEventTarget = <T extends Event>(
}, [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]);
};

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect, useRef } from "react";
import React, { FC, useEffect, useRef } from "react";
import { Participant } from "../room/InCallView";
import { useCallFeed } from "./useCallFeed";
@@ -29,19 +29,22 @@ interface AudioForParticipantProps {
audioDestination: AudioNode;
}
export function AudioForParticipant({
export const AudioForParticipant: FC<AudioForParticipantProps> = ({
item,
audioContext,
audioDestination,
}: AudioForParticipantProps): JSX.Element {
const { stream, localVolume, audioMuted } = useCallFeed(item.callFeed);
}) => {
const { stream, localVolume } = useCallFeed(item.callFeed);
const [audioTrackCount] = useMediaStreamTrackCount(stream);
const gainNodeRef = useRef<GainNode>();
const sourceRef = useRef<MediaStreamAudioSourceNode>();
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) {
gainNodeRef.current = new GainNode(audioContext, {
gain: localVolume,
@@ -68,12 +71,11 @@ export function AudioForParticipant({
audioDestination,
stream,
localVolume,
audioMuted,
audioTrackCount,
]);
return null;
}
};
interface AudioContainerProps {
items: Participant[];
@@ -81,10 +83,7 @@ interface AudioContainerProps {
audioDestination: AudioNode;
}
export function AudioContainer({
items,
...rest
}: AudioContainerProps): JSX.Element {
export const AudioContainer: FC<AudioContainerProps> = ({ items, ...rest }) => {
return (
<>
{items
@@ -94,4 +93,4 @@ export function AudioContainer({
))}
</>
);
}
};

View File

@@ -111,7 +111,8 @@ const getPipGap = (gridAspectRatio: number): number =>
function getTilePositions(
tileCount: number,
presenterTileCount: number,
focusedTileCount: number,
hasPresenter: boolean,
gridWidth: number,
gridHeight: number,
pipXRatio: number,
@@ -119,7 +120,7 @@ function getTilePositions(
layout: Layout
): TilePosition[] {
if (layout === "freedom") {
if (tileCount === 2 && presenterTileCount === 0) {
if (tileCount === 2 && !hasPresenter) {
return getOneOnOneLayoutTilePositions(
gridWidth,
gridHeight,
@@ -130,7 +131,7 @@ function getTilePositions(
return getFreedomLayoutTilePositions(
tileCount,
presenterTileCount,
focusedTileCount,
gridWidth,
gridHeight
);
@@ -247,7 +248,7 @@ function getSpotlightLayoutTilePositions(
function getFreedomLayoutTilePositions(
tileCount: number,
presenterTileCount: number,
focusedTileCount: number,
gridWidth: number,
gridHeight: number
): TilePosition[] {
@@ -261,7 +262,7 @@ function getFreedomLayoutTilePositions(
const { layoutDirection, itemGridRatio } = getGridLayout(
tileCount,
presenterTileCount,
focusedTileCount,
gridWidth,
gridHeight
);
@@ -277,7 +278,7 @@ function getFreedomLayoutTilePositions(
itemGridHeight = gridHeight;
}
const itemTileCount = tileCount - presenterTileCount;
const itemTileCount = tileCount - focusedTileCount;
const {
columnCount: itemColumnCount,
@@ -295,65 +296,55 @@ function getFreedomLayoutTilePositions(
);
const itemGridBounds = getSubGridBoundingBox(itemGridPositions);
let presenterGridWidth;
let presenterGridHeight;
let focusedGridWidth: number;
let focusedGridHeight: number;
if (presenterTileCount === 0) {
presenterGridWidth = 0;
presenterGridHeight = 0;
if (focusedTileCount === 0) {
focusedGridWidth = 0;
focusedGridHeight = 0;
} else if (layoutDirection === "vertical") {
presenterGridWidth = gridWidth;
presenterGridHeight =
focusedGridWidth = gridWidth;
focusedGridHeight =
gridHeight - (itemGridBounds.height + (itemTileCount ? GAP * 2 : 0));
} else {
presenterGridWidth =
focusedGridWidth =
gridWidth - (itemGridBounds.width + (itemTileCount ? GAP * 2 : 0));
presenterGridHeight = gridHeight;
focusedGridHeight = gridHeight;
}
const {
columnCount: presenterColumnCount,
rowCount: presenterRowCount,
tileAspectRatio: presenterTileAspectRatio,
} = getSubGridLayout(
presenterTileCount,
presenterGridWidth,
presenterGridHeight
columnCount: focusedColumnCount,
rowCount: focusedRowCount,
tileAspectRatio: focusedTileAspectRatio,
} = getSubGridLayout(focusedTileCount, focusedGridWidth, focusedGridHeight);
const focusedGridPositions = getSubGridPositions(
focusedTileCount,
focusedColumnCount,
focusedRowCount,
focusedTileAspectRatio,
focusedGridWidth,
focusedGridHeight
);
const presenterGridPositions = getSubGridPositions(
presenterTileCount,
presenterColumnCount,
presenterRowCount,
presenterTileAspectRatio,
presenterGridWidth,
presenterGridHeight
);
const tilePositions = [...focusedGridPositions, ...itemGridPositions];
const tilePositions = [...presenterGridPositions, ...itemGridPositions];
centerTiles(
presenterGridPositions,
presenterGridWidth,
presenterGridHeight,
0,
0
);
centerTiles(focusedGridPositions, focusedGridWidth, focusedGridHeight, 0, 0);
if (layoutDirection === "vertical") {
centerTiles(
itemGridPositions,
gridWidth,
gridHeight - presenterGridHeight,
gridHeight - focusedGridHeight,
0,
presenterGridHeight
focusedGridHeight
);
} else {
centerTiles(
itemGridPositions,
gridWidth - presenterGridWidth,
gridWidth - focusedGridWidth,
gridHeight,
presenterGridWidth,
focusedGridWidth,
0
);
}
@@ -418,14 +409,14 @@ function isMobileBreakpoint(gridWidth: number, gridHeight: number): boolean {
function getGridLayout(
tileCount: number,
presenterTileCount: number,
focusedTileCount: number,
gridWidth: number,
gridHeight: number
): { itemGridRatio: number; layoutDirection: LayoutDirection } {
let layoutDirection: LayoutDirection = "horizontal";
let itemGridRatio = 1;
if (presenterTileCount === 0) {
if (focusedTileCount === 0) {
return { itemGridRatio, layoutDirection };
}
@@ -663,29 +654,25 @@ function getSubGridPositions(
// Sets the 'order' property on tiles based on the layout param and
// other properties of the tiles, eg. 'focused' and 'presenter'
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
tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1));
} else {
const focusedTiles: Tile[] = [];
const presenterTiles: Tile[] = [];
const otherTiles: Tile[] = [];
const orderedTiles: Tile[] = new Array(tiles.length);
tiles.forEach((tile) => (orderedTiles[tile.order] = tile));
orderedTiles.forEach((tile) =>
(tile.focused
? focusedTiles
: tile.presenter
? presenterTiles
: otherTiles
).push(tile)
(tile.focused ? focusedTiles : otherTiles).push(tile)
);
[...focusedTiles, ...presenterTiles, ...otherTiles].forEach(
(tile, i) => (tile.order = i)
);
[...focusedTiles, ...otherTiles].forEach((tile, i) => (tile.order = i));
}
}
@@ -759,11 +746,8 @@ export function VideoGrid({
}
let focused: boolean;
let presenter = false;
if (layout === "spotlight") {
focused = item.focused;
presenter = item.presenter;
} else {
focused = layout === lastLayoutRef.current ? tile.focused : false;
}
@@ -774,7 +758,7 @@ export function VideoGrid({
item,
remove,
focused,
presenter,
presenter: item.presenter,
});
}
@@ -795,7 +779,7 @@ export function VideoGrid({
item,
remove: false,
focused: layout === "spotlight" && item.focused,
presenter: layout === "spotlight" && item.presenter,
presenter: item.presenter,
};
if (existingTile) {
@@ -821,7 +805,7 @@ export function VideoGrid({
.map((tile) => ({ ...tile })); // clone before reordering
reorderTiles(newTiles, layout);
const presenterTileCount = newTiles.reduce(
const focusedTileCount = newTiles.reduce(
(count, tile) => count + (tile.focused ? 1 : 0),
0
);
@@ -831,7 +815,8 @@ export function VideoGrid({
tiles: newTiles,
tilePositions: getTilePositions(
newTiles.length,
presenterTileCount,
focusedTileCount,
newTiles.some((t) => t.presenter),
gridBounds.width,
gridBounds.height,
pipXRatio,
@@ -843,7 +828,7 @@ export function VideoGrid({
}, 250);
}
const presenterTileCount = newTiles.reduce(
const focusedTileCount = newTiles.reduce(
(count, tile) => count + (tile.focused ? 1 : 0),
0
);
@@ -855,7 +840,8 @@ export function VideoGrid({
tiles: newTiles,
tilePositions: getTilePositions(
newTiles.length,
presenterTileCount,
focusedTileCount,
newTiles.some((t) => t.presenter),
gridBounds.width,
gridBounds.height,
pipXRatio,
@@ -959,7 +945,7 @@ export function VideoGrid({
const item = tile.item;
setTileState(({ tiles, ...state }) => {
let presenterTileCount = 0;
let focusedTileCount = 0;
const newTiles = tiles.map((tile) => {
const newTile = { ...tile }; // clone before reordering
@@ -967,7 +953,7 @@ export function VideoGrid({
newTile.focused = !tile.focused;
}
if (newTile.focused) {
presenterTileCount++;
focusedTileCount++;
}
return newTile;
@@ -980,7 +966,8 @@ export function VideoGrid({
tiles: newTiles,
tilePositions: getTilePositions(
newTiles.length,
presenterTileCount,
focusedTileCount,
newTiles.some((t) => t.presenter),
gridBounds.width,
gridBounds.height,
pipXRatio,
@@ -1012,7 +999,7 @@ export function VideoGrid({
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
if (!dragTile.item.isLocal) return;

View File

@@ -17,6 +17,7 @@ limitations under the License.
import React, { forwardRef } from "react";
import { animated } from "@react-spring/web";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import styles from "./VideoTile.module.css";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
@@ -66,6 +67,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
},
ref
) => {
const { t } = useTranslation();
const toolbarButtons: JSX.Element[] = [];
if (!isLocal) {
toolbarButtons.push(
@@ -111,7 +114,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
{!maximised &&
(screenshare ? (
<div className={styles.presenterLabel}>
<span>{`${name} is presenting`}</span>
<span>{t("{{name}} is presenting", { name })}</span>
</div>
) : (
<div className={classNames(styles.infoBubble, styles.memberName)}>

View File

@@ -16,6 +16,7 @@ limitations under the License.
import React, { ChangeEvent, useState } from "react";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { useTranslation } from "react-i18next";
import { FieldRow } from "../input/Input";
import { Modal } from "../Modal";
@@ -61,10 +62,12 @@ interface Props {
}
export const VideoTileSettingsModal = ({ feed, ...rest }: Props) => {
const { t } = useTranslation();
return (
<Modal
className={styles.videoTileSettingsModal}
title="Local volume"
title={t("Local volume")}
isDismissable
mobileFullScreen
{...rest}

View File

@@ -22,7 +22,7 @@ import { WidgetApi, MatrixCapabilities } from "matrix-widget-api";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { IWidgetApiRequest } from "matrix-widget-api";
import { LazyEventEmitter } from "./LazyEventEmitter";
import { getRoomParams } from "./room/useRoomParams";
import { getUrlParams } from "./UrlParams";
// Subset of the actions in matrix-react-sdk
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
// 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 (!userId) throw new Error("User 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
const sendState = [
@@ -119,7 +120,7 @@ export const widget: WidgetHelpers | null = (() => {
},
roomId,
{
baseUrl: "",
baseUrl,
userId,
deviceId,
timelineSupport: true,

View File

@@ -29,7 +29,7 @@ export default defineConfig(({ mode }) => {
},
plugins: [
svgrPlugin(),
htmlTemplate({
htmlTemplate.default({
data: {
title: env.VITE_PRODUCT_NAME || "Matrix Video Chat",
},

978
yarn.lock

File diff suppressed because it is too large Load Diff