├── .dockerignore ├── .env.example ├── .eslintrc.cjs ├── .githooks ├── post-commit └── pre-commit ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── enhancement.yml ├── release.yml └── workflows │ ├── blocked.yaml │ ├── build-and-publish-docker.yaml │ ├── build-element-call.yaml │ ├── build.yaml │ ├── changelog-label.yml │ ├── deploy-to-netlify.yaml │ ├── lint.yaml │ ├── pr-deploy.yaml │ ├── publish-embedded-packages.yaml │ ├── publish.yaml │ ├── test.yaml │ ├── translations-download.yaml │ └── translations-upload.yaml ├── .gitignore ├── .node-version ├── .postcssrc.json ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── .yarn └── plugins │ └── linker.cjs ├── .yarnrc.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE-AGPL-3.0 ├── LICENSE-COMMERCIAL ├── README.md ├── WIDGET_TEST.md ├── babel.config.cjs ├── backend ├── dev_homeserver.yaml ├── dev_livekit.yaml ├── dev_nginx.conf ├── dev_tls_local-ca.crt ├── dev_tls_local-ca.key ├── dev_tls_m.localhost.crt ├── dev_tls_m.localhost.key ├── dev_tls_setup ├── ew.test.config.json ├── playwright_homeserver.yaml └── redis.conf ├── codecov.yaml ├── config ├── config.devenv.json ├── config.sample.json ├── config_netlify_preview.json ├── httpd.conf ├── netlify_redirects ├── nginx.conf └── otel_dev │ ├── README.md │ ├── collector-gateway.yaml │ ├── docker-compose.yaml │ └── nginx_otel.conf ├── demo.gif ├── dev-backend-docker-compose.yml ├── docs ├── Federated_Setup.drawio.png ├── MSC4195_setup.drawio.png ├── README.md ├── SFU_selection.drawio.png ├── controls.md ├── element_call_standalone.drawio.png ├── element_call_widget.drawio.png ├── embedded-standalone.md ├── embedded_package.drawio.png ├── full_package.drawio.png ├── linking.md ├── self-hosting.md └── url-params.md ├── embedded ├── android │ ├── .gitattributes │ ├── .gitignore │ ├── gradle.properties │ ├── gradle │ │ ├── libs.versions.toml │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── lib │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── kotlin │ │ │ └── io │ │ │ └── element │ │ │ └── android │ │ │ └── call │ │ │ └── embedded │ │ │ └── Version.kt │ ├── publish_android_package.sh │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── LICENSE-AGPL-3.0 │ ├── LICENSE-COMMMERCIAL │ ├── Package.swift │ ├── README.md │ └── Sources │ │ └── EmbeddedElementCall │ │ └── EmbeddedElementCall.swift └── web │ ├── LICENSE-AGPL-3.0 │ ├── LICENSE-COMMERCIAL │ ├── README.md │ └── package.json ├── i18next-parser.config.ts ├── index.html ├── knip.ts ├── localazy.json ├── locales ├── bg │ └── app.json ├── cs │ └── app.json ├── da │ └── app.json ├── de │ └── app.json ├── el │ └── app.json ├── en │ └── app.json ├── es │ └── app.json ├── et │ └── app.json ├── fa │ └── app.json ├── fi │ └── app.json ├── fr │ └── app.json ├── id │ └── app.json ├── it │ └── app.json ├── ja │ └── app.json ├── lv │ └── app.json ├── pl │ └── app.json ├── ro │ └── app.json ├── ru │ └── app.json ├── sk │ └── app.json ├── sv │ └── app.json ├── tr │ └── app.json ├── uk │ └── app.json ├── vi │ └── app.json ├── zh-Hans │ └── app.json └── zh-Hant │ └── app.json ├── package.json ├── playwright-backend-docker-compose.override.yml ├── playwright-backend-docker-compose.yml ├── playwright.config.ts ├── playwright ├── access.spec.ts ├── create-call.spec.ts ├── errors.spec.ts ├── fixtures │ └── widget-user.ts ├── global.d.ts ├── landing.spec.ts └── widget │ └── simple-create.spec.ts ├── public └── favicon.png ├── renovate.json ├── scripts ├── dockerbuild.sh ├── playwright-webserver-command.sh └── reformat-release-notes.py ├── src ├── @types │ ├── global.d.ts │ ├── i18next.d.ts │ ├── matrix-js-sdk.d.ts │ └── modules.d.ts ├── App.tsx ├── Avatar.test.tsx ├── Avatar.tsx ├── ClientContext.tsx ├── DisconnectedBanner.module.css ├── DisconnectedBanner.tsx ├── ErrorView.module.css ├── ErrorView.tsx ├── FullScreenView.module.css ├── FullScreenView.tsx ├── Header.module.css ├── Header.test.tsx ├── Header.tsx ├── IndexedDBWorker.ts ├── LazyEventEmitter.ts ├── Modal.module.css ├── Modal.test.tsx ├── Modal.tsx ├── Overlay.module.css ├── Platform.ts ├── QrCode.module.css ├── QrCode.test.tsx ├── QrCode.tsx ├── RTCConnectionStats.module.css ├── RTCConnectionStats.tsx ├── RichError.tsx ├── Slider.module.css ├── Slider.tsx ├── Toast.module.css ├── Toast.test.tsx ├── Toast.tsx ├── TranslatedError.ts ├── UrlParams.test.ts ├── UrlParams.ts ├── UserMenu.module.css ├── UserMenu.tsx ├── UserMenuContainer.tsx ├── __snapshots__ │ ├── Modal.test.tsx.snap │ ├── QrCode.test.tsx.snap │ ├── Toast-test.tsx.snap │ └── Toast.test.tsx.snap ├── analytics │ ├── AnalyticsNotice.tsx │ ├── PosthogAnalytics.test.ts │ ├── PosthogAnalytics.ts │ ├── PosthogEvents.ts │ ├── PosthogSpanProcessor.ts │ └── RageshakeSpanProcessor.ts ├── auth │ ├── LoginPage.module.css │ ├── LoginPage.tsx │ ├── RegisterPage.tsx │ ├── generateRandomName.ts │ ├── useInteractiveLogin.ts │ ├── useInteractiveRegistration.ts │ ├── useRecaptcha.ts │ └── useRegisterPasswordlessUser.ts ├── button │ ├── Button.module.css │ ├── Button.tsx │ ├── InviteButton.tsx │ ├── Link.module.css │ ├── Link.tsx │ ├── LinkButton.tsx │ ├── ReactionToggleButton.module.css │ ├── ReactionToggleButton.test.tsx │ ├── ReactionToggleButton.tsx │ ├── __snapshots__ │ │ └── ReactionToggleButton.test.tsx.snap │ └── index.ts ├── config │ ├── Config.ts │ └── ConfigOptions.ts ├── controls.ts ├── e2ee │ ├── e2eeType.ts │ ├── matrixKeyProvider.test.ts │ ├── matrixKeyProvider.ts │ └── sharedKeyManagement.ts ├── form │ ├── Form.module.css │ └── Form.tsx ├── graphics │ ├── backgroundGradient.svg │ └── loggedOutGradient.svg ├── grid │ ├── CallLayout.ts │ ├── Grid.css │ ├── Grid.module.css │ ├── Grid.tsx │ ├── GridLayout.module.css │ ├── GridLayout.tsx │ ├── OneOnOneLayout.module.css │ ├── OneOnOneLayout.tsx │ ├── SpotlightExpandedLayout.module.css │ ├── SpotlightExpandedLayout.tsx │ ├── SpotlightLandscapeLayout.module.css │ ├── SpotlightLandscapeLayout.tsx │ ├── SpotlightPortraitLayout.module.css │ ├── SpotlightPortraitLayout.tsx │ ├── TileWrapper.module.css │ └── TileWrapper.tsx ├── home │ ├── CallList.module.css │ ├── CallList.test.tsx │ ├── CallList.tsx │ ├── HomePage.tsx │ ├── JoinExistingCallModal.module.css │ ├── JoinExistingCallModal.tsx │ ├── RegisteredView.module.css │ ├── RegisteredView.tsx │ ├── UnauthenticatedView.module.css │ ├── UnauthenticatedView.tsx │ ├── common.module.css │ └── useGroupCallRooms.ts ├── icons │ ├── Check.svg │ ├── Chevron.svg │ ├── Close.svg │ ├── LockOff.svg │ ├── Login.svg │ ├── Logo.svg │ ├── LogoLarge.svg │ ├── LogoMark.svg │ ├── LogoType.svg │ ├── Logout.svg │ ├── Settings.svg │ ├── StarSelected.svg │ ├── StarUnselected.svg │ └── User.svg ├── index.css ├── initializer.test.ts ├── initializer.tsx ├── input │ ├── AvatarInputField.module.css │ ├── AvatarInputField.tsx │ ├── FeedbackInput.module.css │ ├── Input.module.css │ ├── Input.tsx │ ├── StarRating.test.tsx │ ├── StarRatingInput.module.css │ └── StarRatingInput.tsx ├── livekit │ ├── BlurBackgroundTransformer.ts │ ├── MatrixAudioRenderer.test.tsx │ ├── MatrixAudioRenderer.tsx │ ├── MediaDevicesContext.tsx │ ├── TrackProcessorContext.tsx │ ├── openIDSFU.ts │ ├── options.ts │ ├── useECConnectionState.test.tsx │ ├── useECConnectionState.ts │ └── useLivekit.ts ├── main.tsx ├── mediapipe │ └── imageSegmenter │ │ ├── README.md │ │ └── selfie_segmenter.tflite ├── otel │ ├── OTelCall.ts │ ├── OTelCallAbstractMediaStreamSpan.ts │ ├── OTelCallFeedMediaStreamSpan.ts │ ├── OTelCallMediaStreamTrackSpan.ts │ ├── OTelCallTransceiverMediaStreamSpan.ts │ ├── OTelGroupCallMembership.ts │ ├── ObjectFlattener.test.ts │ ├── ObjectFlattener.ts │ ├── otel.test.ts │ └── otel.ts ├── profile │ └── useProfile.ts ├── reactions │ ├── RaisedHandIndicator.test.tsx │ ├── RaisedHandIndicator.tsx │ ├── ReactionIndicator.module.css │ ├── ReactionIndicator.tsx │ ├── ReactionsReader.test.tsx │ ├── ReactionsReader.ts │ ├── __snapshots__ │ │ └── RaisedHandIndicator.test.tsx.snap │ ├── index.ts │ └── useReactionsSender.tsx ├── room │ ├── AppSelectionModal.module.css │ ├── AppSelectionModal.tsx │ ├── CallEndedView.module.css │ ├── CallEndedView.tsx │ ├── CallEventAudioRenderer.test.tsx │ ├── CallEventAudioRenderer.tsx │ ├── EncryptionLock.module.css │ ├── EncryptionLock.tsx │ ├── GroupCallErrorBoundary.test.tsx │ ├── GroupCallErrorBoundary.tsx │ ├── GroupCallView.test.tsx │ ├── GroupCallView.tsx │ ├── InCallView.module.css │ ├── InCallView.test.tsx │ ├── InCallView.tsx │ ├── InviteModal.module.css │ ├── InviteModal.test.tsx │ ├── InviteModal.tsx │ ├── LayoutToggle.module.css │ ├── LayoutToggle.tsx │ ├── LobbyView.module.css │ ├── LobbyView.tsx │ ├── MuteStates.test.tsx │ ├── MuteStates.ts │ ├── RageshakeRequestModal.tsx │ ├── ReactionAudioRenderer.test.tsx │ ├── ReactionAudioRenderer.tsx │ ├── ReactionsOverlay.module.css │ ├── ReactionsOverlay.test.tsx │ ├── ReactionsOverlay.tsx │ ├── RoomAuthView.module.css │ ├── RoomAuthView.tsx │ ├── RoomPage.tsx │ ├── VideoPreview.module.css │ ├── VideoPreview.test.tsx │ ├── VideoPreview.tsx │ ├── __snapshots__ │ │ ├── GroupCallErrorBoundary.test.tsx.snap │ │ └── InCallView.test.tsx.snap │ ├── checkForParallelCalls.test.ts │ ├── checkForParallelCalls.ts │ ├── useActiveFocus.ts │ ├── useJoinRule.ts │ ├── useLoadGroupCall.ts │ ├── useRoomAvatar.ts │ ├── useRoomName.ts │ ├── useRoomState.ts │ └── useSwitchCamera.ts ├── rtcSessionHelpers.test.ts ├── rtcSessionHelpers.ts ├── settings │ ├── DeveloperSettingsTab.module.css │ ├── DeveloperSettingsTab.tsx │ ├── DeviceSelection.module.css │ ├── DeviceSelection.tsx │ ├── FeedbackSettingsTab.tsx │ ├── PreferencesSettingsTab.tsx │ ├── ProfileSettingsTab.module.css │ ├── ProfileSettingsTab.tsx │ ├── RageshakeButton.module.css │ ├── RageshakeButton.tsx │ ├── SettingsModal.module.css │ ├── SettingsModal.tsx │ ├── rageshake.ts │ ├── settings.ts │ ├── submit-rageshake.test.ts │ ├── submit-rageshake.ts │ └── useSubmitRageshake.test.tsx ├── sound │ ├── LICENCE.md │ ├── blocked.mp3 │ ├── blocked.ogg │ ├── end_talk.mp3 │ ├── end_talk.ogg │ ├── join_call.mp3 │ ├── join_call.ogg │ ├── left_call.mp3 │ ├── left_call.ogg │ ├── raise_hand.mp3 │ ├── raise_hand.ogg │ ├── reactions │ │ ├── cat.mp3 │ │ ├── cat.ogg │ │ ├── clap.mp3 │ │ ├── clap.ogg │ │ ├── crickets.mp3 │ │ ├── crickets.ogg │ │ ├── deer.mp3 │ │ ├── deer.ogg │ │ ├── dog.mp3 │ │ ├── dog.ogg │ │ ├── generic.mp3 │ │ ├── generic.ogg │ │ ├── lightbulb.mp3 │ │ ├── lightbulb.ogg │ │ ├── party.mp3 │ │ ├── party.ogg │ │ ├── rock.mp3 │ │ ├── rock.ogg │ │ ├── wave.mp3 │ │ └── wave.ogg │ ├── screen_share_started.mp3 │ ├── screen_share_started.ogg │ ├── start_talk_local.mp3 │ ├── start_talk_local.ogg │ ├── start_talk_remote.mp3 │ └── start_talk_remote.ogg ├── soundUtils.ts ├── state │ ├── CallViewModel.test.ts │ ├── CallViewModel.ts │ ├── GridLikeLayout.ts │ ├── MediaViewModel.test.ts │ ├── MediaViewModel.ts │ ├── MuteAllAudioModel.test.ts │ ├── MuteAllAudioModel.ts │ ├── ObservableScope.ts │ ├── OneOnOneLayout.ts │ ├── PipLayout.ts │ ├── SpotlightExpandedLayout.ts │ ├── TileStore.ts │ ├── TileViewModel.ts │ ├── ViewModel.ts │ ├── observeSpeaker.test.ts │ └── observeSpeaker.ts ├── tabs │ ├── Tabs.module.css │ └── Tabs.tsx ├── tile │ ├── GridTile.module.css │ ├── GridTile.test.tsx │ ├── GridTile.tsx │ ├── MediaView.module.css │ ├── MediaView.test.tsx │ ├── MediaView.tsx │ ├── SpotlightTile.module.css │ ├── SpotlightTile.test.tsx │ ├── SpotlightTile.tsx │ ├── TileAvatar.module.css │ ├── TileAvatar.test.tsx │ └── TileAvatar.tsx ├── useAudioContext.test.tsx ├── useAudioContext.tsx ├── useCallViewKeyboardShortcuts.test.tsx ├── useCallViewKeyboardShortcuts.ts ├── useErrorBoundary.test.tsx ├── useErrorBoundary.ts ├── useEvents.test.tsx ├── useEvents.ts ├── useInitial.ts ├── useLatest.ts ├── useLocalStorage.test.tsx ├── useLocalStorage.ts ├── useMatrixRTCSessionJoinState.ts ├── useMatrixRTCSessionMemberships.ts ├── useMediaQuery.ts ├── useMergedRefs.ts ├── usePageTitle.ts ├── usePrefersReducedMotion.ts ├── useReactiveState.ts ├── useTheme.test.ts ├── useTheme.ts ├── useWakeLock.ts ├── utils │ ├── abortHandle.ts │ ├── array.ts │ ├── displayname-integration.test.ts │ ├── displayname.test.ts │ ├── displayname.ts │ ├── errors.ts │ ├── fetch.test.ts │ ├── fetch.ts │ ├── iter.test.ts │ ├── iter.ts │ ├── matrix.ts │ ├── media.ts │ ├── observable.ts │ ├── spa.ts │ ├── test-fixtures.ts │ ├── test-viewmodel.ts │ └── test.ts ├── vitest.setup.ts └── widget.ts ├── tsconfig.json ├── vite-embedded.config.js ├── vite.config.js ├── vitest.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #### 2 | # Build-time app config 3 | # Environment files are documented here: 4 | # https://vitejs.dev/guide/env-and-mode.html#env-files 5 | #### 6 | # Develop backend settings: 7 | LIVEKIT_KEY="devkey" 8 | LIVEKIT_SECRET="secret" 9 | 10 | # Used for determining the homeserver to use for short urls etc. 11 | # VITE_FALLBACK_STUN_ALLOWED=false 12 | 13 | # CSS to be injected into the page for the purpose of custom theming. 14 | # Generally, writing a custom theme involves overriding Compound design tokens, 15 | # which are documented here: 16 | # https://compound.element.io/?path=/docs/foundations-design-tokens--docs 17 | # https://compound.element.io/?path=/docs/tokens-color-palettes--docs 18 | # https://compound.element.io/?path=/docs/tokens-semantic-colors--docs 19 | # VITE_CUSTOM_CSS=".cpd-theme-dark.cpd-theme-dark { --cpd-color-theme-bg: #101317; }" 20 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const COPYRIGHT_HEADER = `/* 2 | Copyright %%CURRENT_YEAR%% New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | `; 9 | 10 | module.exports = { 11 | plugins: ["matrix-org", "rxjs"], 12 | extends: [ 13 | "plugin:matrix-org/react", 14 | "plugin:matrix-org/a11y", 15 | "plugin:matrix-org/typescript", 16 | "prettier", 17 | "plugin:rxjs/recommended", 18 | ], 19 | parserOptions: { 20 | ecmaVersion: "latest", 21 | sourceType: "module", 22 | project: ["./tsconfig.json"], 23 | }, 24 | env: { 25 | browser: true, 26 | node: true, 27 | }, 28 | rules: { 29 | "matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER], 30 | "jsx-a11y/media-has-caption": "off", 31 | // We should use the js-sdk logger, never console directly. 32 | "no-console": ["error"], 33 | "react/display-name": "error", 34 | // Encourage proper usage of Promises: 35 | "@typescript-eslint/no-floating-promises": "error", 36 | "@typescript-eslint/no-misused-promises": "error", 37 | "@typescript-eslint/promise-function-async": "error", 38 | "@typescript-eslint/require-await": "error", 39 | "@typescript-eslint/await-thenable": "error", 40 | // To help ensure that we get proper vite/rollup lazy loading (e.g. for matrix-js-sdk): 41 | "@typescript-eslint/consistent-type-imports": [ 42 | "error", 43 | { fixStyle: "inline-type-imports" }, 44 | ], 45 | // To encourage good usage of RxJS: 46 | "rxjs/no-exposed-subjects": "error", 47 | "rxjs/finnish": "error", 48 | }, 49 | settings: { 50 | react: { 51 | version: "detect", 52 | }, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /.githooks/post-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | 3 | FILE=.links.temp-disabled.yaml 4 | if test -f "$FILE"; then 5 | # Only do the post-commit hook if the file was temp-disabled by the pre-commit hook. 6 | # Otherwise linking was actively (`yarn links:disable`) disabled and this hook should noop. 7 | mv .links.temp-disabled.yaml .links.yaml 8 | yarnLog=$(yarn) 9 | echo "[yarn-linker] The post-commit hook has re-enabled .links.yaml." 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | 3 | FILE=".links.yaml" 4 | if test -f "$FILE"; then 5 | mv .links.yaml .links.temp-disabled.yaml 6 | # echo "running yarn" 7 | x=$(yarn) 8 | y=$(git add yarn.lock) 9 | echo "[yarn-linker] The pre-commit hook has disabled .links.yaml and MODIFIED the yarn.lock file. Review the staged changes (the hook added yarn.lock, was this desired?) and run \`git commit \` again if they look okay. The post-commit hook will re-enable your links." 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @element-hq/element-call-reviewers 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions & support 4 | url: https://matrix.to/#/#webrtc:matrix.org 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.yml: -------------------------------------------------------------------------------- 1 | name: Enhancement request 2 | description: Do you have a suggestion or feature request? 3 | labels: [T-Enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to propose a new feature or make a suggestion. 9 | - type: textarea 10 | id: usecase 11 | attributes: 12 | label: Your use case 13 | description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups. 14 | placeholder: Tell us what you would like to do! 15 | value: | 16 | #### What would you like to do? 17 | 18 | #### Why would you like to do it? 19 | 20 | #### How would you like to achieve it? 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: alternative 25 | attributes: 26 | label: Have you considered any alternatives? 27 | placeholder: A clear and concise description of any alternative solutions or features you've considered. 28 | validations: 29 | required: false 30 | - type: textarea 31 | id: additional-context 32 | attributes: 33 | label: Additional context 34 | placeholder: Is there anything else you'd like to add? 35 | validations: 36 | required: false 37 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: 🛠 Breaking Changes 4 | labels: 5 | - PR-Breaking-Change 6 | - title: ✨ Features 7 | labels: 8 | - PR-Feature 9 | - title: 🙌 Improvements 10 | labels: 11 | - PR-Improvement 12 | - title: 📄 Documentation 13 | labels: 14 | - PR-Documentation 15 | - title: 🐛 Bugfixes 16 | labels: 17 | - PR-Bug-Fix 18 | - title: 💾 Developer Experience 19 | labels: 20 | - PR-Developer-Experience 21 | - title: Others 22 | labels: 23 | - "*" 24 | exclude: 25 | labels: 26 | - PR-Task 27 | - dependencies 28 | - title: 👒 Dependencies 29 | labels: 30 | - dependencies 31 | -------------------------------------------------------------------------------- /.github/workflows/blocked.yaml: -------------------------------------------------------------------------------- 1 | name: Prevent blocked 2 | on: 3 | pull_request_target: 4 | types: [opened, labeled, unlabeled, synchronize] 5 | jobs: 6 | prevent-blocked: 7 | name: Prevent blocked 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: read 11 | steps: 12 | - name: Add notice 13 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 14 | if: contains(github.event.pull_request.labels.*.name, 'X-Blocked') 15 | with: 16 | script: | 17 | core.setFailed("PR has been labeled with X-Blocked; it cannot be merged."); 18 | -------------------------------------------------------------------------------- /.github/workflows/changelog-label.yml: -------------------------------------------------------------------------------- 1 | name: PR changelog label 2 | 3 | on: 4 | pull_request_target: 5 | types: [labeled, unlabeled, opened] 6 | jobs: 7 | pr-changelog-label: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2 11 | with: 12 | REQUIRED_LABELS_ANY: "PR-Bug-Fix,PR-Documentation,PR-Task,PR-Feature,PR-Improvement,PR-Developer-Experience,dependencies" 13 | REQUIRED_LABELS_ANY_DESCRIPTION: "Select at least one 'PR-' label" 14 | BANNED_LABELS: "banned" 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint, format & type check 2 | on: 3 | pull_request: {} 4 | jobs: 5 | prettier: 6 | name: Lint, format & type check 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 11 | - name: Enable Corepack 12 | run: corepack enable 13 | - name: Yarn cache 14 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 15 | with: 16 | cache: "yarn" 17 | node-version-file: ".node-version" 18 | - name: Install dependencies 19 | run: "yarn install --immutable" 20 | - name: Prettier 21 | run: "yarn run prettier:check" 22 | - name: i18n 23 | run: "yarn run i18n:check" 24 | - name: ESLint 25 | run: "yarn run lint:eslint" 26 | - name: Type check 27 | run: "yarn run lint:types" 28 | - name: Dead code analysis 29 | run: "yarn run lint:knip" 30 | -------------------------------------------------------------------------------- /.github/workflows/translations-download.yaml: -------------------------------------------------------------------------------- 1 | name: Download translation files from Localazy 2 | on: 3 | workflow_dispatch: 4 | secrets: 5 | ELEMENT_BOT_TOKEN: 6 | required: true 7 | 8 | jobs: 9 | download: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | 14 | steps: 15 | - name: Checkout the code 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | 18 | - name: Enable Corepack 19 | run: corepack enable 20 | 21 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 22 | with: 23 | cache: "yarn" 24 | node-version-file: ".node-version" 25 | 26 | - name: Install Deps 27 | run: "yarn install --immutable" 28 | 29 | - name: Prune i18n 30 | run: "rm -R locales" 31 | 32 | - name: Download translation files 33 | uses: localazy/download@0a79880fb66150601e3b43606fab69c88123c087 # v1.1.0 34 | with: 35 | groups: "-p includeSourceLang:true" 36 | 37 | - name: Fix the owner of the downloaded files 38 | run: "sudo chown runner:docker -R locales" 39 | 40 | - name: Prettier 41 | run: yarn prettier:format 42 | 43 | - name: Create Pull Request 44 | id: cpr 45 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 46 | with: 47 | token: ${{ secrets.ELEMENT_BOT_TOKEN }} 48 | branch: actions/localazy-download 49 | delete-branch: true 50 | title: Localazy Download 51 | commit-message: Translations updates 52 | labels: | 53 | T-Task 54 | 55 | - name: Enable automerge 56 | run: gh pr merge --merge --auto "$PR_NUMBER" 57 | if: steps.cpr.outputs.pull-request-operation == 'created' 58 | env: 59 | GH_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} 60 | PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }} 61 | -------------------------------------------------------------------------------- /.github/workflows/translations-upload.yaml: -------------------------------------------------------------------------------- 1 | name: Upload translation files to Localazy 2 | on: 3 | push: 4 | branches: 5 | - livekit 6 | paths-ignore: 7 | - ".github/**" 8 | 9 | jobs: 10 | upload: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | 15 | steps: 16 | - name: Checkout the code 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 18 | 19 | - name: Upload 20 | uses: localazy/upload@27e6b5c0fddf4551596b42226b1c24124335d24a # v1 21 | with: 22 | write_key: ${{ secrets.LOCALAZY_WRITE_KEY }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .env 4 | dist 5 | dist-ssr 6 | *.local 7 | *.bkp 8 | .idea/ 9 | public/config.json 10 | backend/synapse_tmp/* 11 | /coverage 12 | 13 | # Yarn 14 | yarn-error.log 15 | /.pnp.* 16 | /.yarn/* 17 | !/.yarn/patches 18 | !/.yarn/plugins 19 | !/.yarn/releases 20 | !/.yarn/sdks 21 | !/.yarn/versions 22 | /.links.yaml 23 | /.links.disabled.yaml 24 | /.links.temp-disabled.yaml 25 | 26 | # Playwright 27 | /test-results/ 28 | /playwright-report/ 29 | /blob-report/ 30 | /playwright/.cache/ -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.postcssrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "postcss-preset-env": { 4 | "stage": 3, 5 | "browsers": "last 2 versions" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.insertSpaces": true, 5 | "editor.tabSize": 2, 6 | "[typescriptreact]": { 7 | "editor.formatOnSave": true, 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[javascriptreact]": { 11 | "editor.formatOnSave": true, 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[typescript]": { 15 | "editor.formatOnSave": true, 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[javascript]": { 19 | "editor.formatOnSave": true, 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | plugins: 3 | - .yarn/plugins/linker.cjs 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing code to Element 2 | 3 | Element follows the same pattern as the [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md). 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine AS builder 2 | 3 | COPY ./dist /dist 4 | 5 | # Compress assets to work with nginx-gzip-static-module 6 | WORKDIR /dist/assets 7 | RUN gzip -k ../index.html *.js *.map *.css *.wasm *-app-*.json 8 | 9 | FROM nginxinc/nginx-unprivileged:alpine-slim 10 | 11 | COPY --from=builder ./dist /app 12 | 13 | COPY config/nginx.conf /etc/nginx/conf.d/default.conf 14 | -------------------------------------------------------------------------------- /LICENSE-COMMERCIAL: -------------------------------------------------------------------------------- 1 | Licensees holding a valid commercial license with Element may use this 2 | software in accordance with the terms contained in a written agreement 3 | between you and Element. 4 | 5 | To purchase a commercial license please contact our sales team at 6 | licensing@element.io 7 | -------------------------------------------------------------------------------- /WIDGET_TEST.md: -------------------------------------------------------------------------------- 1 | # Testing Element-Call in widget mode 2 | 3 | When running `yarn backend` the latest element-web develop will be deployed and served on `http://localhost:8081`. 4 | In a development environment, you might prefer to just use the `element-web` repo directly, but this setup is useful for CI/CD testing. 5 | 6 | ## Setup 7 | 8 | The element-web configuration is modified to: 9 | 10 | - Enable to use the local widget instance (`element_call.url` https://localhost:3000). 11 | - Enable the labs features (`feature_group_calls`, `feature_element_call_video_rooms`). 12 | 13 | The default configuration used by docker-compose is in `test-container/config.json`. There is a fixture for playwright 14 | that uses 15 | 16 | ## Running the element-web instance 17 | 18 | It is part of the existing backend setup. To start the backend, run: 19 | 20 | ```sh 21 | yarn backend 22 | ``` 23 | 24 | Then open `http://localhost:8081` in your browser. 25 | 26 | ## Basic fixture 27 | 28 | A base fixture is provided in `/playwright/fixtures/widget-user.ts` that will register two users that shares a room. 29 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | [ 12 | "@babel/preset-react", 13 | { 14 | runtime: "automatic", 15 | }, 16 | ], 17 | "@babel/preset-typescript", 18 | ], 19 | plugins: ["babel-plugin-transform-vite-meta-env"], 20 | }; 21 | -------------------------------------------------------------------------------- /backend/dev_homeserver.yaml: -------------------------------------------------------------------------------- 1 | server_name: "synapse.m.localhost" 2 | public_baseurl: https://synapse.m.localhost/ 3 | 4 | pid_file: /data/homeserver.pid 5 | 6 | listeners: 7 | - port: 8008 8 | tls: false 9 | type: http 10 | x_forwarded: true 11 | resources: 12 | - names: [client, federation, openid] 13 | compress: false 14 | 15 | database: 16 | name: sqlite3 17 | args: 18 | database: /data/homeserver.db 19 | 20 | media_store_path: /data/media_store 21 | signing_key_path: "/data/SERVERNAME.signing.key" 22 | trusted_key_servers: 23 | - server_name: "matrix.org" 24 | 25 | experimental_features: 26 | # MSC3266: Room summary API. Used for knocking over federation 27 | msc3266_enabled: true 28 | # MSC4222 needed for syncv2 state_after. This allow clients to 29 | # correctly track the state of the room. 30 | msc4222_enabled: true 31 | 32 | # The maximum allowed duration by which sent events can be delayed, as 33 | # per MSC4140. Must be a positive value if set. Defaults to no 34 | # duration (null), which disallows sending delayed events. 35 | max_event_delay_duration: 24h 36 | 37 | # Ratelimiting settings for client actions (registration, login, messaging). 38 | # 39 | # Each ratelimiting configuration is made of two parameters: 40 | # - per_second: number of requests a client can send per second. 41 | # - burst_count: number of requests a client can send before being throttled. 42 | 43 | rc_message: 44 | # This needs to match at least the heart-beat frequency plus a bit of headroom 45 | # Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s 46 | per_second: 0.5 47 | burst_count: 30 48 | 49 | # Required for Element Call in Single Page Mode due to on-the-fly user registration 50 | enable_registration: true 51 | enable_registration_without_verification: true 52 | 53 | report_stats: false 54 | serve_server_wellknown: true 55 | -------------------------------------------------------------------------------- /backend/dev_livekit.yaml: -------------------------------------------------------------------------------- 1 | port: 7880 2 | bind_addresses: 3 | - "0.0.0.0" 4 | rtc: 5 | tcp_port: 7881 6 | port_range_start: 50100 7 | port_range_end: 50200 8 | use_external_ip: false 9 | #redis: 10 | # address: redis:6379 11 | # username: "" 12 | # password: "" 13 | # db: 0 14 | turn: 15 | enabled: false 16 | domain: localhost 17 | cert_file: "" 18 | key_file: "" 19 | tls_port: 5349 20 | udp_port: 443 21 | external_tls: true 22 | keys: 23 | devkey: secret 24 | -------------------------------------------------------------------------------- /backend/dev_tls_local-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDGjCCAgKgAwIBAgIUGdiFHhH4KL2pqBjMQHQ+PVIkSV8wDQYJKoZIhvcNAQEL 3 | BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMDMy 4 | MDJaFw0zNTA1MDMxMDMyMDJaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg 5 | Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDA2y0hjmNn1vRsVSdy 6 | 8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH9jdT 7 | tG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeFw9fw 8 | eRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZNWnie 9 | Ui7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAveL9K 10 | FGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BLPiQU 11 | KGKrAgMBAAGjUDBOMB0GA1UdDgQWBBQJqBjMu61c1p24txw/y+kv3D+V6DAfBgNV 12 | HSMEGDAWgBQJqBjMu61c1p24txw/y+kv3D+V6DAMBgNVHRMEBTADAQH/MA0GCSqG 13 | SIb3DQEBCwUAA4IBAQB8m2YfFGLugNt5vAAOvNxVqDA8c72yCVYr3CBCpmTIEY5Z 14 | d3qVGhG9//ux6+J8ntkSwd9nV5GJyYXHukCG1VavnAWolWdNF/WAllf0jhLuz7kD 15 | /cJnuI1By4tBsBmSz851i6HJ4t5k99Be+6GQVzi0e7zzfxTHZE4xP2J6Ox8QbPsP 16 | n0m76nIp/WbWaJqzvIIjJhmUUPPv+4wN+eOArgjiGLzptM2qTtGZtd0c9nS5gvep 17 | +mEbSUN9zkhAroZf80wf+hEvy+fJ94VbZ9QjTzTg7odZLrsXGIe8DaG63EYRQ25b 18 | W5iYBAreln5fGSt7qHsGfqwZibTEk/Lx3dydO1Kg 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /backend/dev_tls_local-ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDA2y0hjmNn1vRs 3 | VSdy8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH 4 | 9jdTtG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeF 5 | w9fweRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZN 6 | WnieUi7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAv 7 | eL9KFGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BL 8 | PiQUKGKrAgMBAAECggEAAPX2kxi5AQ7ul82SzT1KgpSXyDHLdYaUyAoYnaX9RO+B 9 | 8ylmpyeqygs4+KQS4EMJm9jpo85Oy37bIKdG3kljU6wQcKlL5Y+ZUOo1nzpV6fid 10 | hGVs6ts8VXw8KshKQ9AyccZ8L/pirUfgOffgTwfjY7/90zceAL/s98GuZWc62nkX 11 | 55joQv/OikqYfAGP/U6Bp2Zyf23DwJB09Z3B6NnZj/ZyAbDrDEHuA15LhCOcCczp 12 | IU/mFEywBPHT9Tg4w4Beq78PeAETvku2UalYRLhP3RLlXr2oEbwUtINRVt2QjZ85 13 | Esps4uCqL/mgQluIebtudD9HL/YMlNPXue1mDXFxJQKBgQDgZZY4yJBcf488T1V6 14 | HNm06b/LvVGj253pKgw14hpY1xQu3Ymgzv1GEqzhSYdzxhpmj0tMUNHxAp+YdGQu 15 | SZ0wcPKhw0aYVkIjDRYDC3Wn5GJhyIEYHGYMo/n4l49UzHRBPOTDzp49DkHTKBgh 16 | XgIIazYT3CkjTIMRrkUv+qfIPQKBgQDcBGu/mqbjxs4sN3zqPS4aB21o6t6W0sXs 17 | ZP9w6RlTPQi5U2oRbftjZtYc0bbEgkMUImB1HwYPQT5pJ+MyC414xDvSc2exBr5d 18 | To6yyPIy78Tf5PHM12fpKV92nSvoz/pSjYcGxxDtKfPqu+t8mOJfjCV1lLLA+xuB 19 | DDaE4p8dBwKBgQCdAne6A5v/HMH8UQZeCxHJpESvKiiVnnU/UEx651nID7XvlNNX 20 | 0X0mKqsMd4ZvW43ddSYan/JF0LAa3FW8jYWO/3jF9vzOWoysOdvNBZetgf/Uq5ao 21 | aDZ/YbzmVCXWD7jIbPMkjs3pqrAkL0mzDzQc7+dGviWKrV6IYIfIqnn7gQKBgDCz 22 | vdIk/qpO+JZrFfiX4Fucp0hhLTJ/p5ZDaRPqVVPKn+K+Jy2ChfIj8mNgvK9VEloj 23 | nexvGJ1J2PHYBX+vdPp1nbRhHWPfVUY8PHQw7QP/dToGaMvqJrNDGEGeWvjnCMc7 24 | UtdaO1H0Rm0AegkTopB56lTTvJnhO95eALd7nrMDAoGAEPdzJtWoKafp49svhSj0 25 | hiXQv2SPBwVUN4LZ4SOWiXUcmYYm80aNpYKLkBxYjrfqFWhE7NUHLGp8YorQWKY2 26 | acD9AReHk/xku0ABy6jeYmSCmCxASxst5liKD+l12sk0gB0rk5MBxB4Uu1MIbQZ2 27 | aCASX3AVD2/XyC2MKkzc8Eg= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /backend/dev_tls_m.localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDZzCCAk+gAwIBAgIUXizLjwkdqepX0bh0K3abeJxj68IwDQYJKoZIhvcNAQEL 3 | BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMzU5 4 | MTFaFw0zNTA1MDMxMzU5MTFaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi 5 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrzGSScSgaQuZdELGFYiLiYRwr 6 | LKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI39WmH95aX3d+A 7 | U7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ouuN6AOdzWs8hp 8 | RYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFft1ZXxIZOCjDs 9 | rEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71IfieLCEzMTP1VXa 10 | tP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZrQUGnL2RtAgMB 11 | AAGjgaIwgZ8wHwYDVR0jBBgwFoAUCagYzLutXNaduLccP8vpL9w/legwCQYDVR0T 12 | BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwMAYDVR0RBCkw 13 | J4IJbG9jYWxob3N0ggttLmxvY2FsaG9zdIINKi5tLmxvY2FsaG9zdDAdBgNVHQ4E 14 | FgQUfdh1p52ZgWyZcBgBXGwKi4EnUE0wDQYJKoZIhvcNAQELBQADggEBAKrHEuB6 15 | 33j8+EwSHw3zrvt/DRXK2BDHI1Ir9JcztSunaKAjZXVvf/dvZp0Xs1dEdJIdnv6G 16 | iZYhBbOqDqpQZbf2h/h0kuu5yZSBUdnQXnYNxlhp2UaC/UEgw5iZT/p1rm7RjVie 17 | y4Dp2WytV5iZOLmLj6xDvd3DXazgJPWIRX8p8qJZbKTkwCjTr7nDIj8jjG1sVFf7 18 | 1RJBO5/6WSnImrpDmlLUrvjiKvbxcdseDJyBOhTwdRdSk4S2M+s5tR5j2I1gXLOq 19 | J5ioN76+SCrTY0K0WKRy9oOXWO1/X3+VYcekp+0F3SGkd5w17jylCv1XIGHAdEsQ 20 | v2z2/aMI/7sAD2Q= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /backend/dev_tls_m.localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrzGSScSgaQuZd 3 | ELGFYiLiYRwrLKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI3 4 | 9WmH95aX3d+AU7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ou 5 | uN6AOdzWs8hpRYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFf 6 | t1ZXxIZOCjDsrEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71Ifi 7 | eLCEzMTP1VXatP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZr 8 | QUGnL2RtAgMBAAECggEAJaFQii8U/KOYt9vXNoMnZvSkaeSQLLhn2V6Kciu1CtWE 9 | aMTWLsFE6nk+G5xXkYcTmM3T0GghtH3u5CjyI6EcsEkeEorCZJt0wbmayDmqiekR 10 | LfMzOdHuTHX5+edPgMGYYG1BFyRKyYFsjH1b5zRFZhXdGQnrl5760GsVlz9D1KZQ 11 | iHcT+q1S2tmZeoUukQnADENKXUMCyTGM5FCddgNtsWnGDsTDayh7hUdvDkB+mW4G 12 | lSp+BZuc3PCwpbD6qkXvfugWs6CUAAtXoV3ceWgxQ+TEnNlwxaG1AyugfgNUBolk 13 | 8xgeZt4r5QId03jsHDf7hpBAofcaCd5EMIIQYFvWoQKBgQDlbAvAzEFPTZZn2nRV 14 | Xagw4xjqVc1LLEKLCWq0N5rEkwn0h90Dz5N7/3NuonP/sIDsDHCbyiOYBI1Ck6Xi 15 | 0WuB+OyKDh+xeF2mekN9G9ywPahdK5lT/TVsxXFyZlwtVv1x/6KBO4yv5URizxqU 16 | gyAPDDxfD/KcNjkOBaodWEwQGQKBgQC/s2gPDBtQkjLwkHXchBomLww5eLlVrac1 17 | WK4UX6uSdOgrjJ375OOgMTxe8NVZdOuAKytGXRWDwgH3nVWvuZhe7dGlX3JMuSer 18 | e9VwDpBESrvqcR4ruL6wm8wej6BXyjH0wD3FHb0S5HfuBDxTn+4bDwrbRzOUMNgy 19 | lSppuflxdQKBgQDiZcIfazFT8evn5nMAvuC4BZNTxIJHmZC9JfjPiUPIkpWzYtOe 20 | 7BvNtKOT3Op9uw8uYYRKqKqBXJSNy6ha8XCXHS9HeXKbLn20SFkLQBCDNwVLlDfF 21 | 40zyXtF6JDr4XyzSb4NM5pgKCER5AYloXxGm59s3sEQpFXUuOjbKqJS/GQKBgAoI 22 | c7vF4HAZFr1sch62cz/oWnVvkhOf4Q5zs7ixQSOLJtOQqnwSgK9TpFs7s47ZBbJR 23 | kBRAru2Ua9Hv1Bo8VnMxczV6h1roneDlvEf/GyHX33nnrbKQGrrXjJlU3wl5NaAf 24 | p5v3cHvapUQ5yIZ/6lBUOzc6xMJOxCHxmKSr7Rg5AoGAbEE4lt6Xh2dnBPJ81eNI 25 | IDrw/3ITY53qAY4Bx88CByIFuu8CEUdUZprh98jSl6ic1tMinZfUhRMwABLrUD51 26 | DGst8iGLPD9u83iMcUHI/L+p7AbxrKLvWXZrF5UZm440c9mSWqfhPaTBosPtNDsG 27 | LfETwH1flKXMTXd2xA9RTE4= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /backend/dev_tls_setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Step 1: Create a Root CA key and cert 4 | openssl genrsa -out dev_tls_local-ca.key 2048 5 | openssl req -x509 -new -nodes \ 6 | -days 3650 \ 7 | -subj "/CN=Element Call Dev CA" \ 8 | -key dev_tls_local-ca.key \ 9 | -out dev_tls_local-ca.crt \ 10 | -sha256 -addext "basicConstraints=CA:TRUE" 11 | 12 | # Step 2: Create a private key and CSR for *.m.localhost 13 | openssl req -new -nodes -newkey rsa:2048 \ 14 | -keyout dev_tls_m.localhost.key \ 15 | -out dev_tls_m.localhost.csr \ 16 | -subj "/CN=*.m.localhost" 17 | 18 | # Step 3: Sign the CSR with your CA 19 | openssl x509 \ 20 | -req -in dev_tls_m.localhost.csr \ 21 | -CA dev_tls_local-ca.crt -CAkey dev_tls_local-ca.key \ 22 | -CAcreateserial \ 23 | -out dev_tls_m.localhost.crt \ 24 | -days 3650 \ 25 | -sha256 \ 26 | -extfile <( cat < 2 | ServerName localhost 3 | 4 | DocumentRoot "/app" 5 | 6 | 7 | # disable cache entriely by default (apart from Etag which is accurate enough) 8 | Header add Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" 9 | CacheDisable on 10 | ExpiresActive off 11 | 12 | # also turn off last-modified since they are just the timestamps of the file in the docker image 13 | # and may or may not bear any resemblance to when the resource changed 14 | Header add Last-Modified "" 15 | 16 | DirectoryIndex index.html 17 | 18 | 19 | # assets can be cached because they have hashed filenames 20 | 21 | ExpiresActive on 22 | ExpiresDefault "access plus 1 week" 23 | Header add Cache-Control "public, no-transform" 24 | 25 | 26 | 27 | ForceType application/json 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /config/netlify_redirects: -------------------------------------------------------------------------------- 1 | # This file is copied to the netlify deploy dir in the upload stage 2 | 3 | # Redirect any unknown path to index.html 4 | /* /index.html 200 5 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | 5 | root /app; 6 | gzip_static on; 7 | gzip_vary on; 8 | 9 | location / { 10 | try_files $uri $uri/ /index.html; 11 | add_header Cache-Control "public, max-age=30, stale-while-revalidate=30"; 12 | } 13 | 14 | # assets can be cached because they have hashed filenames 15 | location /assets { 16 | add_header Cache-Control "public, immutable, max-age=31536000"; 17 | } 18 | 19 | location /apple-app-site-association { 20 | default_type application/json; 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /config/otel_dev/README.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry Collector for development 2 | 3 | This directory contains a docker compose file that starts a jaeger all-in-one instance 4 | with an in-memory database, along with a standalone OpenTelemetry collector that forwards 5 | traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be 6 | configured to send CORS headers so can't be used from a browser. This sets the config on 7 | the collector to send CORS headers. 8 | 9 | This also adds an nginx to add CORS headers to the jaeger query endpoint, such that it can 10 | be used from webapps like stalk (https://deniz.co/stalk/). The CORS enabled endpoint is 11 | exposed on port 16687. To use stalk, you should simply be able to navigate to it and add 12 | http://127.0.0.1:16687/api as a data source. 13 | 14 | (Yes, we could enable the OTLP collector in jaeger all-in-one and passed this through 15 | the nginx to enable CORS too, rather than running a separate collector. There's no reason 16 | it's done this way other than that I'd already set up the separate collector.) 17 | 18 | Running `docker compose up` in this directory should be all you need. 19 | -------------------------------------------------------------------------------- /config/otel_dev/collector-gateway.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | http: 5 | endpoint: 0.0.0.0:4318 6 | cors: 7 | allowed_origins: 8 | # This can't be '*' because opentelemetry-js uses sendBeacon which always operates 9 | # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' 10 | #- "https://pr976--element-call.netlify.app" 11 | - "http://*" 12 | allowed_headers: 13 | - "*" 14 | processors: 15 | batch: 16 | timeout: 1s 17 | resource: 18 | attributes: 19 | - key: test.key 20 | value: "test-value" 21 | action: insert 22 | exporters: 23 | logging: 24 | loglevel: info 25 | jaeger: 26 | endpoint: jaeger-all-in-one:14250 27 | tls: 28 | insecure: true 29 | extensions: 30 | health_check: 31 | pprof: 32 | endpoint: :1888 33 | zpages: 34 | endpoint: :55679 35 | service: 36 | extensions: [pprof, zpages, health_check] 37 | pipelines: 38 | traces: 39 | receivers: [otlp] 40 | processors: [batch, resource] 41 | exporters: [logging, jaeger] 42 | -------------------------------------------------------------------------------- /config/otel_dev/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | # Jaeger 4 | jaeger-all-in-one: 5 | image: jaegertracing/all-in-one:latest 6 | ports: 7 | - "16686:16686" 8 | - "14268" 9 | - "14250" 10 | # Collector 11 | collector-gateway: 12 | image: otel/opentelemetry-collector:latest 13 | volumes: 14 | - ./collector-gateway.yaml:/etc/collector-gateway.yaml 15 | command: ["--config=/etc/collector-gateway.yaml"] 16 | ports: 17 | - "1888:1888" # pprof extension 18 | - "13133:13133" # health_check extension 19 | - "4317:4317" # OTLP gRPC receiver 20 | - "4318:4318" # OTLP HTTP receiver 21 | - "55670:55679" # zpages extension 22 | depends_on: 23 | - jaeger-all-in-one 24 | nginx: 25 | image: nginxinc/nginx-unprivileged:latest 26 | volumes: 27 | - ./nginx_otel.conf:/etc/nginx/conf.d/default.conf:ro 28 | ports: 29 | - "16687:8080" 30 | -------------------------------------------------------------------------------- /config/otel_dev/nginx_otel.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | 5 | location / { 6 | proxy_pass http://jaeger-all-in-one:16686/; 7 | add_header Access-Control-Allow-Origin *; 8 | 9 | if ($request_method = OPTIONS) { 10 | add_header Access-Control-Allow-Origin *; 11 | add_header Content-Type text/plain; 12 | add_header Content-Length 0; 13 | return 204; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/demo.gif -------------------------------------------------------------------------------- /docs/Federated_Setup.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/docs/Federated_Setup.drawio.png -------------------------------------------------------------------------------- /docs/MSC4195_setup.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/docs/MSC4195_setup.drawio.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Element Call Docs 2 | 3 | This folder contains documentation for setup, usage, and development of Element Call. 4 | 5 | - [Embedded vs standalone mode](./embedded-standalone.md) 6 | - [Url format and parameters](./url-params.md) 7 | - [Global JS controls](./controls.md) 8 | - [Self-Hosting](./self-hosting.md) 9 | - [Developing with linked packages](./linking.md) 10 | -------------------------------------------------------------------------------- /docs/SFU_selection.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/docs/SFU_selection.drawio.png -------------------------------------------------------------------------------- /docs/element_call_standalone.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/docs/element_call_standalone.drawio.png -------------------------------------------------------------------------------- /docs/element_call_widget.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/docs/element_call_widget.drawio.png -------------------------------------------------------------------------------- /docs/embedded_package.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/docs/embedded_package.drawio.png -------------------------------------------------------------------------------- /docs/full_package.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/docs/full_package.drawio.png -------------------------------------------------------------------------------- /docs/linking.md: -------------------------------------------------------------------------------- 1 | # Developing with linked packages 2 | 3 | If you want to make changes to a package that Element Call depends on and see those changes applied in real time, you can create a link to a local copy of the package. Yarn has a command for this (`yarn link`), but it's not recommended to use it as it ends up modifying package.json with details specific to your development environment. 4 | 5 | Instead, you can use our little 'linker' plugin. Create a file named `.links.yaml` in the Element Call project directory, listing the names and paths of any dependencies you want to link. For example: 6 | 7 | ```yaml 8 | matrix-js-sdk: ../path/to/matrix-js-sdk 9 | "@vector-im/compound-web": /home/alice/path/to/compound-web 10 | ``` 11 | 12 | Then run `yarn install`. 13 | 14 | ## Hooks 15 | 16 | Changes in `.links.yaml` will also update `yarn.lock` when `yarn` is executed. The lockfile will then contain the local 17 | version of the package which would not work on others dev setups or the github CI. 18 | One always needs to run: 19 | 20 | ```bash 21 | mv .links.yaml .links.disabled.yaml 22 | yarn 23 | ``` 24 | 25 | before committing a change. 26 | 27 | To make it more convenient to work with this linking system we added git hooks for your conviniece. 28 | A `pre-commit` hook will run `mv .links.yaml .links.disabled.yaml`, `yarn` and `git add yarn.lock` if it detects 29 | a `.links.yaml` file and abort the commit. 30 | You will than need to check if the resulting changes are appropriate and commit again. 31 | 32 | A `post-commit` hook will setup the linking as it was 33 | before if a `.links.disabled.yaml` is present. It runs `mv .links.disabled.yaml .links.yaml` and `yarn`. 34 | 35 | To activate the hooks automatically configure git with 36 | 37 | ```bash 38 | git config --local core.hooksPath .githooks/ 39 | ``` 40 | -------------------------------------------------------------------------------- /embedded/android/.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | -------------------------------------------------------------------------------- /embedded/android/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | 7 | # Ignore local gradle properties file 8 | local.properties 9 | 10 | # Also ignore the generated assets for Android 11 | lib/src/main/assets 12 | -------------------------------------------------------------------------------- /embedded/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # This file was generated by the Gradle 'init' task. 2 | # https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties 3 | 4 | org.gradle.parallel=true 5 | org.gradle.caching=true 6 | -------------------------------------------------------------------------------- /embedded/android/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by the Gradle 'init' task. 2 | # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format 3 | 4 | [versions] 5 | android_gradle_plugin = "8.10.1" 6 | 7 | [libraries] 8 | android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" } 9 | 10 | [plugins] 11 | android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" } 12 | maven_publish = { id = "com.vanniktech.maven.publish", version = "0.32.0" } -------------------------------------------------------------------------------- /embedded/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/embedded/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /embedded/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /embedded/android/lib/src/main/kotlin/io/element/android/call/embedded/Version.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 New Vector Ltd. 3 | * 4 | * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | * Please see LICENSE files in the repository root for full details. 6 | */ 7 | 8 | const val VERSION = "0.0.0" 9 | -------------------------------------------------------------------------------- /embedded/android/publish_android_package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is used for local build and testing of the AAR packaging 4 | # In CI we call gradlew directly 5 | 6 | EC_ASSETS_FOLDER=lib/src/main/assets/element-call 7 | CURRENT_DIR=$( dirname -- "${BASH_SOURCE[0]}" ) 8 | 9 | pushd $CURRENT_DIR > /dev/null 10 | 11 | function build_assets() { 12 | echo "Generating Element Call assets..." 13 | pushd ../.. > /dev/null 14 | yarn build 15 | popd > /dev/null 16 | } 17 | 18 | function copy_assets() { 19 | if [ ! -d $EC_ASSETS_FOLDER ]; then 20 | echo "Creating $EC_ASSETS_FOLDER..." 21 | mkdir -p $EC_ASSETS_FOLDER 22 | fi 23 | 24 | echo "Copying generated Element Call assets to the Android project..." 25 | cp -R ../../dist/* $EC_ASSETS_FOLDER 26 | } 27 | 28 | getopts :sh opt 29 | case $opt in 30 | s) 31 | SKIP=1 32 | ;; 33 | h) 34 | echo "-s: will skip building the assets and just publish the library." 35 | exit 0 36 | ;; 37 | esac 38 | 39 | if [ ! $SKIP ]; then 40 | read -p "Do you want to re-build the assets (y/n, defaults to no)? " -n 1 -r 41 | echo "" 42 | if [[ $REPLY =~ ^[Yy]$ ]]; then 43 | build_assets 44 | else 45 | echo "Using existing assets from ../../dist" 46 | fi 47 | copy_assets 48 | elif [ ! -d $EC_ASSETS_FOLDER ]; then 49 | echo "Assets folder at $EC_ASSETS_FOLDER not found. Either build and copy the assets manually or remove the -s flag." 50 | exit 1 51 | fi 52 | 53 | # Exit with an error if the gradle publishing fails 54 | set -e 55 | echo "Publishing the Android project" 56 | 57 | ./gradlew publishAndReleaseToMavenCentral --no-daemon 58 | 59 | popd > /dev/null -------------------------------------------------------------------------------- /embedded/android/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.6/userguide/multi_project_builds.html in the Gradle documentation. 6 | * This project uses @Incubating APIs which are subject to change. 7 | */ 8 | 9 | pluginManagement { 10 | repositories { 11 | gradlePluginPortal() 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.name = "element-call" 18 | include("lib") 19 | -------------------------------------------------------------------------------- /embedded/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | -------------------------------------------------------------------------------- /embedded/ios/LICENSE-COMMMERCIAL: -------------------------------------------------------------------------------- 1 | Licensees holding a valid commercial license with Element may use this 2 | software in accordance with the terms contained in a written agreement 3 | between you and Element. 4 | 5 | To purchase a commercial license please contact our sales team at 6 | licensing@element.io 7 | -------------------------------------------------------------------------------- /embedded/ios/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "EmbeddedElementCall", 8 | platforms: [.iOS(.v17)], 9 | products: [ 10 | .library( 11 | name: "EmbeddedElementCall", 12 | targets: ["EmbeddedElementCall"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "EmbeddedElementCall", 17 | resources: [.copy("../dist")]), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /embedded/ios/README.md: -------------------------------------------------------------------------------- 1 | # Embedded Element Call for Swift 2 | 3 | SwiftPM package containing an embedded build of the Element Call widget. 4 | 5 | ## Copyright & License 6 | 7 | Copyright 2021-2025 New Vector Ltd 8 | 9 | This software is dual-licensed by New Vector Ltd (Element). It can be used either: 10 | 11 | (1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR 12 | 13 | (2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to). 14 | Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses. 15 | -------------------------------------------------------------------------------- /embedded/ios/Sources/EmbeddedElementCall/EmbeddedElementCall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2025 New Vector Ltd. 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | // Please see LICENSE files in the repository root for full details. 6 | // 7 | 8 | import Foundation 9 | 10 | public let appURL = Bundle.module.url(forResource: "index", withExtension: "html", subdirectory: "dist") 11 | 12 | public let bundle = Bundle.module 13 | 14 | public let version = "0.0.0" 15 | -------------------------------------------------------------------------------- /embedded/web/LICENSE-COMMERCIAL: -------------------------------------------------------------------------------- 1 | Licensees holding a valid commercial license with Element may use this 2 | software in accordance with the terms contained in a written agreement 3 | between you and Element. 4 | 5 | To purchase a commercial license please contact our sales team at 6 | licensing@element.io 7 | -------------------------------------------------------------------------------- /embedded/web/README.md: -------------------------------------------------------------------------------- 1 | # Embedded Element Call for Web 2 | 3 | NPM package containing an embedded build of the Element Call widget. 4 | 5 | ## Copyright & License 6 | 7 | Copyright 2021-2025 New Vector Ltd 8 | 9 | This software is dual-licensed by New Vector Ltd (Element). It can be used either: 10 | 11 | (1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR 12 | 13 | (2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to). 14 | Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses. 15 | -------------------------------------------------------------------------------- /embedded/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@element-hq/element-call-embedded", 3 | "version": "0.0.0", 4 | "files": [ 5 | "README.md", 6 | "LICENSE-AGPL-3.0", 7 | "LICENSE-COMMERCIAL", 8 | "dist" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/element-hq/element-call.git" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /i18next-parser.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | keySeparator: ".", 3 | namespaceSeparator: false, 4 | contextSeparator: "|", 5 | pluralSeparator: "_", 6 | createOldCatalogs: false, 7 | defaultNamespace: "app", 8 | lexers: { 9 | ts: [ 10 | { 11 | lexer: "JavascriptLexer", 12 | functions: ["t", "translatedError"], 13 | namespaceFunctions: ["useTranslation", "withTranslation"], 14 | }, 15 | ], 16 | tsx: [ 17 | { 18 | lexer: "JsxLexer", 19 | functions: ["t", "translatedError"], 20 | namespaceFunctions: ["useTranslation", "withTranslation"], 21 | }, 22 | ], 23 | }, 24 | locales: ["en"], 25 | output: "locales/$LOCALE/$NAMESPACE.json", 26 | input: ["src/**/*.{ts,tsx}"], 27 | sort: true, 28 | }; 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <% if (packageType === "full") { %> 7 | 8 | 9 | <% } %> 10 | 11 | 15 | <%- brand %> 16 | 19 | 20 | <% if (packageType === "full") { %> 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | <% } %> 39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /knip.ts: -------------------------------------------------------------------------------- 1 | import { KnipConfig } from "knip"; 2 | 3 | export default { 4 | vite: { 5 | config: ["vite.config.js", "vite-embedded.config.js"], 6 | }, 7 | entry: ["src/main.tsx", "i18next-parser.config.ts"], 8 | ignoreBinaries: [ 9 | // This is deprecated, so Knip doesn't actually recognize it as a globally 10 | // installed binary. TODO We should switch to Compose v2: 11 | // https://docs.docker.com/compose/migrate/ 12 | "docker-compose", 13 | ], 14 | ignoreDependencies: [ 15 | // Used in CSS 16 | "normalize.css", 17 | // Used for its global type declarations 18 | "@types/grecaptcha", 19 | // Because we use matrix-js-sdk as a Git dependency rather than consuming 20 | // the proper release artifacts, and also import directly from src/, we're 21 | // forced to re-install some of the types that it depends on even though 22 | // these look unused to Knip 23 | "@types/content-type", 24 | "@types/sdp-transform", 25 | "@types/uuid", 26 | // We obviously use this, but if the package has been linked with yarn link, 27 | // then Knip will flag it as a false positive 28 | // https://github.com/webpro-nl/knip/issues/766 29 | "@vector-im/compound-web", 30 | // We need this so that TypeScript is happy with @livekit/track-processors. 31 | // This might be a bug in the LiveKit repo but for now we fix it on the 32 | // Element Call side. 33 | "@types/dom-mediacapture-transform", 34 | "matrix-widget-api", 35 | ], 36 | ignoreExportsUsedInFile: true, 37 | } satisfies KnipConfig; 38 | -------------------------------------------------------------------------------- /localazy.json: -------------------------------------------------------------------------------- 1 | { 2 | "readKey": "a7580769542256117579-70975387172511848f4c6533943d776547bad4853931ba352ee684b738f4494e", 3 | 4 | "upload": { 5 | "type": "json", 6 | "deprecate": "file", 7 | "features": ["plural_postfix_us", "filter_untranslated"], 8 | "files": [ 9 | { 10 | "pattern": "locales/en/*.json", 11 | "lang": "inherited" 12 | }, 13 | { 14 | "group": "existing", 15 | "pattern": "locales/*/*.json", 16 | "excludes": ["locales/en/*.json"], 17 | "lang": "${autodetectLang}" 18 | } 19 | ] 20 | }, 21 | 22 | "download": { 23 | "files": [ 24 | { 25 | "output": "locales/${langLsrDash}/${file}" 26 | } 27 | ], 28 | "includeSourceLang": "${includeSourceLang|false}" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /playwright-backend-docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | services: 2 | synapse: 3 | volumes: 4 | - ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z 5 | -------------------------------------------------------------------------------- /playwright-backend-docker-compose.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - dev-backend-docker-compose.yml 3 | -------------------------------------------------------------------------------- /playwright/global.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import type * as Matrix from "matrix-js-sdk"; 9 | 10 | declare global { 11 | interface Window { 12 | mxMatrixClientPeg: { 13 | get(): Matrix.MatrixClient; 14 | }; 15 | mxSettingsStore: { 16 | setValue: ( 17 | settingKey: string, 18 | room: string | null, 19 | level: string, 20 | setting: string, 21 | ) => void; 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /playwright/landing.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import { test, expect } from "@playwright/test"; 9 | 10 | test("has title", async ({ page }) => { 11 | await page.goto("/"); 12 | 13 | await expect(page).toHaveTitle(/Element Call/); 14 | }); 15 | 16 | test("Landing page", async ({ page }) => { 17 | await page.goto("/"); 18 | 19 | // There should be a login button in the header 20 | await expect(page.getByRole("link", { name: "Log In" })).toBeVisible(); 21 | 22 | await expect( 23 | page.getByRole("heading", { name: "Start new call" }), 24 | ).toBeVisible(); 25 | 26 | await expect(page.getByTestId("home_callName")).toBeVisible(); 27 | await expect(page.getByTestId("home_displayName")).toBeVisible(); 28 | 29 | await expect(page.getByTestId("home_go")).toBeVisible(); 30 | }); 31 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/element-hq/element-call/13fac57b012d374c7e9de389788ab77423c369ea/public/favicon.png -------------------------------------------------------------------------------- /scripts/dockerbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | export VITE_APP_VERSION=$(git describe --tags --abbrev=0) 6 | 7 | corepack enable 8 | yarn install 9 | yarn run build 10 | -------------------------------------------------------------------------------- /scripts/playwright-webserver-command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -n "$USE_DOCKER" ]; then 3 | set -ex 4 | yarn build 5 | docker build -t element-call:testing . 6 | exec docker run --rm --name element-call-testing -p 8080:8080 -v ./config/config.devenv.json:/app/config.json:ro,Z element-call:testing 7 | else 8 | cp config/config.devenv.json public/config.json 9 | exec yarn dev 10 | fi 11 | -------------------------------------------------------------------------------- /scripts/reformat-release-notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This script can be used to reformat the release notes generated by 4 | # GitHub releases into a format slightly more appropriate for our 5 | # project (ie. we don't really need to mention the author of every PR) 6 | # 7 | # eg. in: * OpenTelemetry by @dbkr in https://github.com/vector-im/element-call/pull/961 8 | # out: * OpenTelemetry (https://github.com/vector-im/element-call/pull/961) 9 | 10 | import sys 11 | import re 12 | 13 | for line in sys.stdin: 14 | matches = re.match(r'^\* (.*) by (\S+) in (\S+)$', line.strip()) 15 | print("* %s (%s)" % (matches[1], matches[3])) 16 | -------------------------------------------------------------------------------- /src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import { type setLogLevel as setLKLogLevel } from "livekit-client"; 9 | 10 | import type { DurationFormat as PolyfillDurationFormat } from "@formatjs/intl-durationformat"; 11 | import { type Controls } from "../controls"; 12 | 13 | declare global { 14 | interface Document { 15 | // Safari only supports this prefixed, so tell the type system about it 16 | webkitExitFullscreen: () => void; 17 | webkitFullscreenElement: HTMLElement | null; 18 | } 19 | 20 | interface Window { 21 | controls: Controls; 22 | setLKLogLevel: typeof setLKLogLevel; 23 | } 24 | 25 | interface HTMLElement { 26 | // Safari only supports this prefixed, so tell the type system about it 27 | webkitRequestFullscreen: () => void; 28 | } 29 | 30 | namespace Intl { 31 | // Add DurationFormat as part of the Intl namespace because we polyfill it 32 | const DurationFormat: typeof PolyfillDurationFormat; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/@types/i18next.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import "i18next"; 9 | // import all namespaces (for the default language, only) 10 | import type app from "../../locales/en/app.json"; 11 | 12 | declare module "i18next" { 13 | interface CustomTypeOptions { 14 | defaultNS: "app"; 15 | keySeparator: "."; 16 | resources: { 17 | app: typeof app; 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/@types/matrix-js-sdk.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import { 9 | type ElementCallReactionEventType, 10 | type ECallReactionEventContent, 11 | } from "../reactions"; 12 | 13 | // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types 14 | declare module "matrix-js-sdk/lib/types" { 15 | export interface TimelineEvents { 16 | [ElementCallReactionEventType]: ECallReactionEventContent; 17 | } 18 | 19 | export interface AccountDataEvents { 20 | // Analytics account data event 21 | "im.vector.analytics": { 22 | id: string; 23 | pseudonymousAnalyticsOptIn?: boolean; 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/@types/modules.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | /// 9 | /// 10 | -------------------------------------------------------------------------------- /src/DisconnectedBanner.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | .banner { 9 | position: absolute; 10 | padding: 29px; 11 | background-color: var(--cpd-color-bg-subtle-primary); 12 | vertical-align: middle; 13 | font-size: var(--font-size-body); 14 | text-align: center; 15 | z-index: 1; 16 | top: 76px; 17 | width: calc(100% - 58px); 18 | } 19 | -------------------------------------------------------------------------------- /src/DisconnectedBanner.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import classNames from "classnames"; 9 | import { type FC, type HTMLAttributes, type ReactNode } from "react"; 10 | import { useTranslation } from "react-i18next"; 11 | 12 | import styles from "./DisconnectedBanner.module.css"; 13 | import { type ValidClientState, useClientState } from "./ClientContext"; 14 | 15 | interface Props extends HTMLAttributes { 16 | children?: ReactNode; 17 | className?: string; 18 | } 19 | 20 | export const DisconnectedBanner: FC = ({ 21 | children, 22 | className, 23 | ...rest 24 | }) => { 25 | const { t } = useTranslation(); 26 | const clientState = useClientState(); 27 | let shouldShowBanner = false; 28 | 29 | if (clientState?.state === "valid") { 30 | const validClientState = clientState as ValidClientState; 31 | shouldShowBanner = validClientState.disconnected; 32 | } 33 | 34 | return ( 35 | <> 36 | {shouldShowBanner && ( 37 |
38 | {children} 39 | {t("disconnected_banner")} 40 |
41 | )} 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/ErrorView.module.css: -------------------------------------------------------------------------------- 1 | .error { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | gap: var(--cpd-space-2x); 6 | max-inline-size: 480px; 7 | } 8 | 9 | .icon { 10 | margin-block-end: var(--cpd-space-4x); 11 | } 12 | 13 | .error > h1 { 14 | margin: 0; 15 | } 16 | 17 | .error > p { 18 | font: var(--cpd-font-body-lg-regular); 19 | color: var(--cpd-color-text-secondary); 20 | text-align: center; 21 | } 22 | -------------------------------------------------------------------------------- /src/FullScreenView.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | .page { 9 | position: relative; 10 | display: flex; 11 | flex-direction: column; 12 | overflow: hidden; 13 | min-height: 100%; 14 | } 15 | 16 | .header { 17 | padding: 76px 65px; 18 | } 19 | 20 | .container { 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | flex: 1; 26 | } 27 | 28 | .content { 29 | display: flex; 30 | flex-direction: column; 31 | justify-content: center; 32 | align-items: center; 33 | max-width: 660px; 34 | padding: 0 65px; 35 | } 36 | 37 | .content > * { 38 | margin-top: 0; 39 | margin-bottom: 32px; 40 | } 41 | 42 | .content > :last-child { 43 | margin-bottom: 0; 44 | } 45 | 46 | /* Make the buttons the same width */ 47 | .wideButton { 48 | width: 291px; 49 | } 50 | 51 | /* Fixed height to avoid content jumping around*/ 52 | .sendLogsSection { 53 | height: 50px; 54 | } 55 | -------------------------------------------------------------------------------- /src/Header.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import { expect, test } from "vitest"; 9 | import { render, screen } from "@testing-library/react"; 10 | import { axe } from "vitest-axe"; 11 | import { TooltipProvider } from "@vector-im/compound-web"; 12 | 13 | import { RoomHeaderInfo } from "./Header"; 14 | 15 | test("RoomHeaderInfo is accessible", async () => { 16 | const { container } = render( 17 | 18 | 25 | , 26 | ); 27 | expect(await axe(container)).toHaveNoViolations(); 28 | // Check that the room name acts as a heading 29 | screen.getByRole("heading", { name: "Mission Control" }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/IndexedDBWorker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import { IndexedDBStoreWorker } from "matrix-js-sdk/lib/indexeddb-worker"; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | const remoteWorker = new IndexedDBStoreWorker((self as any).postMessage); 12 | 13 | self.onmessage = remoteWorker.onMessage; 14 | -------------------------------------------------------------------------------- /src/Overlay.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | .bg { 9 | position: fixed; 10 | inset: 0; 11 | background: rgba(3, 12, 27, 0.528); 12 | } 13 | 14 | @keyframes fade-in { 15 | from { 16 | opacity: 0; 17 | } 18 | to { 19 | opacity: 1; 20 | } 21 | } 22 | 23 | .bg.animate[data-state="open"] { 24 | animation: fade-in 200ms; 25 | } 26 | 27 | @keyframes fade-out { 28 | from { 29 | opacity: 1; 30 | } 31 | to { 32 | opacity: 0; 33 | } 34 | } 35 | 36 | .bg.animate[data-state="closed"] { 37 | animation: fade-out 130ms; 38 | } 39 | 40 | .overlay { 41 | position: fixed; 42 | } 43 | 44 | .overlay.animate { 45 | --overlay-top: 50%; 46 | left: 50%; 47 | top: var(--overlay-top); 48 | transform: translate(-50%, -50%); 49 | } 50 | 51 | @keyframes zoom-in { 52 | from { 53 | opacity: 0; 54 | transform: translate(-50%, -50%) scale(80%); 55 | } 56 | to { 57 | opacity: 1; 58 | transform: translate(-50%, -50%) scale(100%); 59 | } 60 | } 61 | 62 | @keyframes zoom-out { 63 | from { 64 | opacity: 1; 65 | transform: translate(-50%, -50%) scale(100%); 66 | } 67 | to { 68 | opacity: 0; 69 | transform: translate(-50%, -50%) scale(80%); 70 | } 71 | } 72 | 73 | .overlay.animate[data-state="open"] { 74 | animation: zoom-in 200ms; 75 | } 76 | 77 | .overlay.animate[data-state="closed"] { 78 | animation: zoom-out 130ms; 79 | } 80 | 81 | @media (prefers-reduced-motion) { 82 | .overlay.animate[data-state="open"] { 83 | animation-name: fade-in; 84 | } 85 | 86 | .overlay.animate[data-state="closed"] { 87 | animation-name: fade-out; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Platform.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | /** 9 | * The platform on which the application is running. 10 | */ 11 | // The granularity of this value is kind of arbitrary: it distinguishes exactly 12 | // the platforms that the app needs to know about in order to correctly 13 | // implement the designs and work around platform-specific browser weirdness. 14 | // Feel free to increase or decrease that granularity in the future as project 15 | // requirements change. 16 | export let platform: "android" | "ios" | "desktop"; 17 | 18 | if (/android/i.test(navigator.userAgent)) { 19 | platform = "android"; 20 | // We include 'Mac' here and double-check for touch support because iPads on 21 | // iOS 13 pretend to be a MacOS desktop 22 | } else if ( 23 | /iPad|iPhone|iPod|Mac/.test(navigator.userAgent) && 24 | "ontouchend" in document 25 | ) { 26 | platform = "ios"; 27 | } else { 28 | platform = "desktop"; 29 | } 30 | 31 | export const isFirefox = (): boolean => { 32 | const { userAgent } = navigator; 33 | return userAgent.includes("Firefox"); 34 | }; 35 | -------------------------------------------------------------------------------- /src/QrCode.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | .qrCode img { 9 | max-width: 100%; 10 | image-rendering: pixelated; 11 | border-radius: var(--cpd-space-4x); 12 | } 13 | -------------------------------------------------------------------------------- /src/QrCode.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import { describe, expect, test } from "vitest"; 9 | import { render, configure } from "@testing-library/react"; 10 | 11 | import { QrCode } from "./QrCode"; 12 | 13 | configure({ 14 | defaultHidden: true, 15 | }); 16 | 17 | describe("QrCode", () => { 18 | test("renders", async () => { 19 | const { container, findByRole } = render( 20 | , 21 | ); 22 | (await findByRole("img")) as HTMLImageElement; 23 | expect(container.firstChild).toMatchSnapshot(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/QrCode.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import { type FC, useEffect, useState } from "react"; 9 | import { toDataURL } from "qrcode"; 10 | import classNames from "classnames"; 11 | import { t } from "i18next"; 12 | 13 | import styles from "./QrCode.module.css"; 14 | 15 | interface Props { 16 | data: string; 17 | className?: string; 18 | } 19 | 20 | export const QrCode: FC = ({ data, className }) => { 21 | const [url, setUrl] = useState(null); 22 | 23 | useEffect(() => { 24 | let isCancelled = false; 25 | 26 | toDataURL(data, { errorCorrectionLevel: "L" }) 27 | .then((url) => { 28 | if (!isCancelled) { 29 | setUrl(url); 30 | } 31 | }) 32 | .catch((reason) => { 33 | if (!isCancelled) { 34 | setUrl(null); 35 | } 36 | }); 37 | 38 | return (): void => { 39 | isCancelled = true; 40 | }; 41 | }, [data]); 42 | 43 | return ( 44 |
45 | {url && {t("qr_code")}} 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/RTCConnectionStats.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | .modal pre { 9 | font-size: var(--font-size-micro); 10 | } 11 | 12 | .statsPill { 13 | border-radius: var(--media-view-border-radius); 14 | grid-area: none; 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | flex-direction: column; 19 | align-items: flex-start; 20 | } 21 | -------------------------------------------------------------------------------- /src/RichError.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import { useTranslation } from "react-i18next"; 9 | import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; 10 | 11 | import type { FC, ReactNode } from "react"; 12 | import { ErrorView } from "./ErrorView"; 13 | import { widget } from "./widget.ts"; 14 | 15 | /** 16 | * An error consisting of a terse message to be logged to the console and a 17 | * richer message to be shown to the user, as a full-screen page. 18 | */ 19 | export class RichError extends Error { 20 | public constructor( 21 | message: string, 22 | /** 23 | * The pretty, more helpful message to be shown on the error screen. 24 | */ 25 | public readonly richMessage: ReactNode, 26 | ) { 27 | super(message); 28 | } 29 | } 30 | 31 | const OpenElsewhere: FC = () => { 32 | const { t } = useTranslation(); 33 | 34 | return ( 35 | 40 |

41 | {t("error.open_elsewhere_description", { 42 | brand: import.meta.env.VITE_PRODUCT_NAME || "Element Call", 43 | })} 44 |

45 |
46 | ); 47 | }; 48 | 49 | export class OpenElsewhereError extends RichError { 50 | public constructor() { 51 | super("App opened in another tab", ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Slider.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | .slider { 9 | display: flex; 10 | align-items: center; 11 | position: relative; 12 | } 13 | 14 | .track { 15 | flex-grow: 1; 16 | border-radius: var(--cpd-radius-pill-effect); 17 | background: var(--cpd-color-bg-subtle-primary); 18 | height: var(--cpd-space-2x); 19 | outline: var(--cpd-border-width-1) solid 20 | var(--cpd-color-border-interactive-primary); 21 | outline-offset: calc(-1 * var(--cpd-border-width-1)); 22 | cursor: pointer; 23 | transition: outline-color ease 0.15s; 24 | } 25 | 26 | .track[data-disabled] { 27 | cursor: initial; 28 | outline-color: var(--cpd-color-border-disabled); 29 | } 30 | 31 | .highlight { 32 | background: var(--cpd-color-bg-action-primary-rest); 33 | position: absolute; 34 | block-size: 100%; 35 | border-radius: var(--cpd-radius-pill-effect); 36 | transition: background-color ease 0.15s; 37 | } 38 | 39 | .highlight[data-disabled] { 40 | background: var(--cpd-color-bg-action-primary-disabled); 41 | } 42 | 43 | .handle { 44 | display: block; 45 | block-size: var(--cpd-space-4x); 46 | inline-size: var(--cpd-space-4x); 47 | border-radius: var(--cpd-radius-pill-effect); 48 | background: var(--cpd-color-bg-action-primary-rest); 49 | box-shadow: 0 0 0 2px var(--cpd-color-bg-canvas-default); 50 | cursor: pointer; 51 | transition: background-color ease 0.15s; 52 | } 53 | 54 | .handle[data-disabled] { 55 | cursor: initial; 56 | background: var(--cpd-color-bg-action-primary-disabled); 57 | } 58 | -------------------------------------------------------------------------------- /src/Toast.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023, 2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | .toast { 9 | color: var(--cpd-color-text-on-solid-primary); 10 | background: var(--cpd-color-alpha-gray-1200); 11 | padding-inline: var(--cpd-space-3x); 12 | padding-block: var(--cpd-space-1x); 13 | border: none; 14 | border-radius: var(--cpd-radius-pill-effect); 15 | box-shadow: var(--small-drop-shadow); 16 | display: flex; 17 | align-items: center; 18 | gap: var(--cpd-space-1x); 19 | } 20 | 21 | .toast > h3 { 22 | margin: 0; 23 | } 24 | 25 | .toast > svg { 26 | color: var(--cpd-color-icon-on-solid-primary); 27 | flex-shrink: 0; 28 | margin-inline-end: calc(-1 * var(--cpd-space-1x)); 29 | } 30 | -------------------------------------------------------------------------------- /src/TranslatedError.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | import type { DefaultNamespace, ParseKeys, TFunction, TOptions } from "i18next"; 9 | 10 | /** 11 | * An error with messages in both English and the user's preferred language. 12 | * Use this for errors that need to be displayed inline within another 13 | * component. For errors that could be given their own screen, prefer 14 | * {@link RichError}. 15 | */ 16 | // Abstract to force consumers to use the function below rather than calling the 17 | // constructor directly 18 | export abstract class TranslatedError extends Error { 19 | /** 20 | * The error message in the user's preferred language. 21 | */ 22 | public readonly translatedMessage: string; 23 | 24 | public constructor( 25 | messageKey: ParseKeys, 26 | translationFn: TFunction, 27 | ) { 28 | super(translationFn(messageKey, { lng: "en" } as TOptions)); 29 | this.translatedMessage = translationFn(messageKey); 30 | } 31 | } 32 | 33 | class TranslatedErrorImpl extends TranslatedError {} 34 | 35 | // i18next-parser can't detect calls to a constructor, so we expose a bare 36 | // function instead 37 | export const translatedError = ( 38 | messageKey: ParseKeys, 39 | t: TFunction<"app", undefined>, 40 | ): TranslatedError => new TranslatedErrorImpl(messageKey, t); 41 | -------------------------------------------------------------------------------- /src/UserMenu.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022-2024 New Vector Ltd. 3 | 4 | SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial 5 | Please see LICENSE in the repository root for full details. 6 | */ 7 | 8 | .menuIcon { 9 | width: 24px; 10 | height: 24px; 11 | 12 | flex-shrink: 0; 13 | } 14 | 15 | .userButton { 16 | appearance: none; 17 | background: none; 18 | border: none; 19 | margin: 0; 20 | cursor: pointer; 21 | } 22 | 23 | .userButton svg * { 24 | fill: var(--cpd-color-icon-primary); 25 | } 26 | -------------------------------------------------------------------------------- /src/__snapshots__/Modal.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`the content is rendered when the modal is open 1`] = ` 4 | 35 | `; 36 | 37 | exports[`the modal renders as a drawer in mobile viewports 1`] = ` 38 |