Apply prettier formatting

pull/23821/head
Michael Weimann 2022-12-09 13:28:29 +01:00
parent a32f12c8f3
commit 7921a6cbf8
No known key found for this signature in database
GPG Key ID: 53F535A266BB9584
104 changed files with 12169 additions and 11047 deletions

View File

@ -1,85 +1,90 @@
module.exports = { module.exports = {
plugins: ["matrix-org"], plugins: ["matrix-org"],
extends: [ extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react"],
"plugin:matrix-org/babel",
"plugin:matrix-org/react",
],
env: { env: {
browser: true, browser: true,
node: true, node: true,
}, },
rules: { rules: {
// Things we do that break the ideal style // Things we do that break the ideal style
"quotes": "off", quotes: "off",
}, },
settings: { settings: {
react: { react: {
version: 'detect', version: "detect",
}, },
}, },
overrides: [{ overrides: [
files: [ {
"src/**/*.{ts,tsx}", files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "module_system/**/*.{ts,tsx}"],
"test/**/*.{ts,tsx}", extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
"module_system/**/*.{ts,tsx}", // NOTE: These rules are frozen and new rules should not be added here.
], // New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
extends: [ rules: {
"plugin:matrix-org/typescript", // Things we do that break the ideal style
"plugin:matrix-org/react", "prefer-promise-reject-errors": "off",
], "quotes": "off",
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
// We disable this while we're transitioning // We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
// We're okay with assertion errors when we ask for them // We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
// Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell. // Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell.
"no-restricted-imports": ["error", { "no-restricted-imports": [
"paths": [{ "error",
"name": "matrix-js-sdk", {
"message": "Please use matrix-js-sdk/src/matrix instead", paths: [
}, { {
"name": "matrix-js-sdk/", name: "matrix-js-sdk",
"message": "Please use matrix-js-sdk/src/matrix instead", message: "Please use matrix-js-sdk/src/matrix instead",
}, { },
"name": "matrix-js-sdk/src", {
"message": "Please use matrix-js-sdk/src/matrix instead", name: "matrix-js-sdk/",
}, { message: "Please use matrix-js-sdk/src/matrix instead",
"name": "matrix-js-sdk/src/", },
"message": "Please use matrix-js-sdk/src/matrix instead", {
}, { name: "matrix-js-sdk/src",
"name": "matrix-js-sdk/src/index", message: "Please use matrix-js-sdk/src/matrix instead",
"message": "Please use matrix-js-sdk/src/matrix instead", },
}, { {
"name": "matrix-react-sdk", name: "matrix-js-sdk/src/",
"message": "Please use matrix-react-sdk/src/index instead", message: "Please use matrix-js-sdk/src/matrix instead",
}, { },
"name": "matrix-react-sdk/", {
"message": "Please use matrix-react-sdk/src/index instead", name: "matrix-js-sdk/src/index",
}], message: "Please use matrix-js-sdk/src/matrix instead",
"patterns": [{ },
"group": ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"], {
"message": "Please use matrix-js-sdk/src/* instead", name: "matrix-react-sdk",
}, { message: "Please use matrix-react-sdk/src/index instead",
"group": ["matrix-react-sdk/lib", "matrix-react-sdk/lib/", "matrix-react-sdk/lib/**"], },
"message": "Please use matrix-react-sdk/src/* instead", {
}], name: "matrix-react-sdk/",
}], message: "Please use matrix-react-sdk/src/index instead",
},
],
patterns: [
{
group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"],
message: "Please use matrix-js-sdk/src/* instead",
},
{
group: ["matrix-react-sdk/lib", "matrix-react-sdk/lib/", "matrix-react-sdk/lib/**"],
message: "Please use matrix-react-sdk/src/* instead",
},
],
},
],
},
}, },
}, { {
files: [ files: ["test/**/*.{ts,tsx}"],
"test/**/*.{ts,tsx}", rules: {
], // We don't need super strict typing in test utilities
rules: { "@typescript-eslint/explicit-function-return-type": "off",
// We don't need super strict typing in test utilities "@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/explicit-function-return-type": "off", },
"@typescript-eslint/explicit-member-accessibility": "off",
}, },
}], ],
}; };

View File

@ -2,75 +2,75 @@ name: Bug report for the Element desktop app (not in a browser)
description: File a bug report if you are using the desktop Element application. description: File a bug report if you are using the desktop Element application.
labels: [T-Defect] labels: [T-Defect]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
Please report security issues by email to security@matrix.org Please report security issues by email to security@matrix.org
- type: textarea - type: textarea
id: reproduction-steps id: reproduction-steps
attributes: attributes:
label: Steps to reproduce label: Steps to reproduce
description: Please attach screenshots, videos or logs if you can. description: Please attach screenshots, videos or logs if you can.
placeholder: Tell us what you see! placeholder: Tell us what you see!
value: | value: |
1. Where are you starting? What can you see? 1. Where are you starting? What can you see?
2. What do you click? 2. What do you click?
3. More steps… 3. More steps…
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: result id: result
attributes: attributes:
label: Outcome label: Outcome
placeholder: Tell us what went wrong placeholder: Tell us what went wrong
value: | value: |
#### What did you expect? #### What did you expect?
#### What happened instead? #### What happened instead?
validations: validations:
required: true required: true
- type: input - type: input
id: os id: os
attributes: attributes:
label: Operating system label: Operating system
placeholder: Windows, macOS, Ubuntu, Arch Linux… placeholder: Windows, macOS, Ubuntu, Arch Linux…
validations: validations:
required: false required: false
- type: input - type: input
id: version id: version
attributes: attributes:
label: Application version label: Application version
description: You can find the version information in Settings -> Help & About. description: You can find the version information in Settings -> Help & About.
placeholder: e.g. Element version 1.7.34, olm version 3.2.3 placeholder: e.g. Element version 1.7.34, olm version 3.2.3
validations: validations:
required: false required: false
- type: input - type: input
id: source id: source
attributes: attributes:
label: How did you install the app? label: How did you install the app?
description: Where did you install the app from? Please give a link or a description. description: Where did you install the app from? Please give a link or a description.
placeholder: e.g. From https://element.io/get-started placeholder: e.g. From https://element.io/get-started
validations: validations:
required: false required: false
- type: input - type: input
id: homeserver id: homeserver
attributes: attributes:
label: Homeserver label: Homeserver
description: | description: |
Which server is your account registered on? If it is a local or non-public homeserver, please tell us what is the homeserver implementation (ex: Synapse/Dendrite/etc.) and the version. Which server is your account registered on? If it is a local or non-public homeserver, please tell us what is the homeserver implementation (ex: Synapse/Dendrite/etc.) and the version.
placeholder: e.g. matrix.org or Synapse 1.50.0rc1 placeholder: e.g. matrix.org or Synapse 1.50.0rc1
validations: validations:
required: false required: false
- type: dropdown - type: dropdown
id: rageshake id: rageshake
attributes: attributes:
label: Will you send logs? label: Will you send logs?
description: | description: |
Did you know that you can send a /rageshake command from your application to submit logs for this issue? Trigger the defect, then type `/rageshake` into the message input area followed by a description of the problem and send the command. You will be able to add a link to this defect report and submit anonymous logs to the developers. Did you know that you can send a /rageshake command from your application to submit logs for this issue? Trigger the defect, then type `/rageshake` into the message input area followed by a description of the problem and send the command. You will be able to add a link to this defect report and submit anonymous logs to the developers.
options: options:
- 'Yes' - "Yes"
- 'No' - "No"
validations: validations:
required: true required: true

View File

@ -2,83 +2,83 @@ name: Bug report for Element Web (in browser)
description: File a bug report if you are using Element in a web browser like Firefox, Chrome, Edge, and so on. description: File a bug report if you are using Element in a web browser like Firefox, Chrome, Edge, and so on.
labels: [T-Defect] labels: [T-Defect]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
Please report security issues by email to security@matrix.org Please report security issues by email to security@matrix.org
- type: textarea - type: textarea
id: reproduction-steps id: reproduction-steps
attributes: attributes:
label: Steps to reproduce label: Steps to reproduce
description: Please attach screenshots, videos or logs if you can. description: Please attach screenshots, videos or logs if you can.
placeholder: Tell us what you see! placeholder: Tell us what you see!
value: | value: |
1. Where are you starting? What can you see? 1. Where are you starting? What can you see?
2. What do you click? 2. What do you click?
3. More steps… 3. More steps…
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: result id: result
attributes: attributes:
label: Outcome label: Outcome
placeholder: Tell us what went wrong placeholder: Tell us what went wrong
value: | value: |
#### What did you expect? #### What did you expect?
#### What happened instead? #### What happened instead?
validations: validations:
required: true required: true
- type: input - type: input
id: os id: os
attributes: attributes:
label: Operating system label: Operating system
placeholder: Windows, macOS, Ubuntu, Arch Linux… placeholder: Windows, macOS, Ubuntu, Arch Linux…
validations: validations:
required: false required: false
- type: input - type: input
id: browser id: browser
attributes: attributes:
label: Browser information label: Browser information
description: Which browser are you using? Which version? description: Which browser are you using? Which version?
placeholder: e.g. Chromium Version 92.0.4515.131 placeholder: e.g. Chromium Version 92.0.4515.131
validations: validations:
required: false required: false
- type: input - type: input
id: webapp-url id: webapp-url
attributes: attributes:
label: URL for webapp label: URL for webapp
description: Which URL are you using to access the webapp? If a private server, tell us what version of Element Web you are using. description: Which URL are you using to access the webapp? If a private server, tell us what version of Element Web you are using.
placeholder: e.g. develop.element.io, app.element.io placeholder: e.g. develop.element.io, app.element.io
validations: validations:
required: false required: false
- type: input - type: input
id: version id: version
attributes: attributes:
label: Application version label: Application version
description: You can find the version information in Settings -> Help & About. description: You can find the version information in Settings -> Help & About.
placeholder: e.g. Element version 1.7.34, olm version 3.2.3 placeholder: e.g. Element version 1.7.34, olm version 3.2.3
validations: validations:
required: false required: false
- type: input - type: input
id: homeserver id: homeserver
attributes: attributes:
label: Homeserver label: Homeserver
description: | description: |
Which server is your account registered on? If it is a local or non-public homeserver, please tell us what is the homeserver implementation (ex: Synapse/Dendrite/etc.) and the version. Which server is your account registered on? If it is a local or non-public homeserver, please tell us what is the homeserver implementation (ex: Synapse/Dendrite/etc.) and the version.
placeholder: e.g. matrix.org or Synapse 1.50.0rc1 placeholder: e.g. matrix.org or Synapse 1.50.0rc1
validations: validations:
required: false required: false
- type: dropdown - type: dropdown
id: rageshake id: rageshake
attributes: attributes:
label: Will you send logs? label: Will you send logs?
description: | description: |
Did you know that you can send a /rageshake command from the web application to submit logs for this issue? Trigger the defect, then type `/rageshake` into the message input area followed by a description of the problem and send the command. You will be able to add a link to this defect report and submit anonymous logs to the developers. Did you know that you can send a /rageshake command from the web application to submit logs for this issue? Trigger the defect, then type `/rageshake` into the message input area followed by a description of the problem and send the command. You will be able to add a link to this defect report and submit anonymous logs to the developers.
options: options:
- 'Yes' - "Yes"
- 'No' - "No"
validations: validations:
required: true required: true

View File

@ -1,5 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Questions & support - name: Questions & support
url: https://matrix.to/#/#element-web:matrix.org url: https://matrix.to/#/#element-web:matrix.org
about: Please ask and answer questions here. about: Please ask and answer questions here.

View File

@ -2,35 +2,35 @@ name: Enhancement request
description: Do you have a suggestion or feature request? description: Do you have a suggestion or feature request?
labels: [T-Enhancement] labels: [T-Enhancement]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/vector-im/element-meta/discussions/new?category=ideas). Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/vector-im/element-meta/discussions/new?category=ideas).
- type: textarea - type: textarea
id: usecase id: usecase
attributes: attributes:
label: Your use case label: Your use case
description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups. description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups.
placeholder: Tell us what you would like to do! placeholder: Tell us what you would like to do!
value: | value: |
#### What would you like to do? #### What would you like to do?
#### Why would you like to do it? #### Why would you like to do it?
#### How would you like to achieve it? #### How would you like to achieve it?
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: alternative id: alternative
attributes: attributes:
label: Have you considered any alternatives? label: Have you considered any alternatives?
placeholder: A clear and concise description of any alternative solutions or features you've considered. placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations: validations:
required: false required: false
- type: textarea - type: textarea
id: additional-context id: additional-context
attributes: attributes:
label: Additional context label: Additional context
placeholder: Is there anything else you'd like to add? placeholder: Is there anything else you'd like to add?
validations: validations:
required: false required: false

View File

@ -2,9 +2,9 @@
## Checklist ## Checklist
* [ ] Tests written for new code (and old code if feasible) - [ ] Tests written for new code (and old code if feasible)
* [ ] Linter and other CI checks pass - [ ] Linter and other CI checks pass
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md)) - [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md))
<!-- <!--
If you would like to specify text for the changelog entry other than your PR title, add the following: If you would like to specify text for the changelog entry other than your PR title, add the following:

View File

@ -1,6 +1,4 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["github>matrix-org/renovate-config-element-web"]
"github>matrix-org/renovate-config-element-web"
]
} }

View File

@ -1,30 +1,30 @@
name: Backport name: Backport
on: on:
pull_request_target: pull_request_target:
types: types:
- closed - closed
- labeled - labeled
branches: branches:
- develop - develop
jobs: jobs:
backport: backport:
name: Backport name: Backport
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only react to merged PRs for security reasons. # Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: > if: >
github.event.pull_request.merged github.event.pull_request.merged
&& ( && (
github.event.action == 'closed' github.event.action == 'closed'
|| ( || (
github.event.action == 'labeled' github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport') && contains(github.event.label.name, 'backport')
) )
) )
steps: steps:
- uses: tibdex/backport@v2 - uses: tibdex/backport@v2
with: with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>" labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR # We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }} github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,26 +1,26 @@
name: Build name: Build
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ master ] branches: [master]
# develop pushes and repository_dispatch handled in build_develop.yaml # develop pushes and repository_dispatch handled in build_develop.yaml
env: env:
# These must be set for fetchdep.sh to get the right branch # These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
build: build:
name: "Build" name: "Build"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Dependencies - name: Install Dependencies
run: "./scripts/layered.sh" run: "./scripts/layered.sh"
- name: Build - name: Build
run: "yarn build" run: "yarn build"

View File

@ -2,114 +2,113 @@
# environment secrets, largely similar to build.yaml. # environment secrets, largely similar to build.yaml.
name: Build and Deploy develop name: Build and Deploy develop
on: on:
push: push:
branches: [ develop ] branches: [develop]
repository_dispatch: repository_dispatch:
types: [ element-web-notify ] types: [element-web-notify]
concurrency: concurrency:
group: ${{ github.repository_owner }}-${{ github.workflow }}-${{ github.ref_name }} group: ${{ github.repository_owner }}-${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
build: build:
name: "Build & Deploy develop.element.io" name: "Build & Deploy develop.element.io"
# Only respect triggers from our develop branch, ignore that of forks # Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'vector-im/element-web' if: github.repository == 'vector-im/element-web'
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: develop environment: develop
env:
R2_BUCKET: 'element-web-develop'
R2_URL: ${{ secrets.CF_R2_S3_API }}
R2_PUBLIC_URL: 'https://element-web-develop.element.io'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- name: Install Dependencies
run: "./scripts/layered.sh"
- name: Build, Package & Upload sourcemaps
run: "./scripts/ci_package.sh"
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} R2_BUCKET: "element-web-develop"
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} R2_URL: ${{ secrets.CF_R2_S3_API }}
SENTRY_URL: ${{ secrets.SENTRY_URL }} R2_PUBLIC_URL: "https://element-web-develop.element.io"
SENTRY_ORG: element steps:
SENTRY_PROJECT: riot-web - uses: actions/checkout@v3
# We only deploy the latest bundles to Cloudflare Pages and use _redirects to fallback to R2 for
# older ones. This redirect means that 'self' is insufficient in the CSP,
# and we have to add the R2 URL.
# Once Cloudflare redirects support proxying mode we will be able to ditch this.
# See Proxying in support table at https://developers.cloudflare.com/pages/platform/redirects
CSP_EXTRA_SOURCE: ${{ env.R2_PUBLIC_URL }}
- run: mv dist/element-*.tar.gz dist/develop.tar.gz - uses: actions/setup-node@v3
with:
cache: "yarn"
- uses: actions/upload-artifact@v3 - name: Install Dependencies
with: run: "./scripts/layered.sh"
name: webapp
path: dist/develop.tar.gz
retention-days: 1
- name: Extract webapp
run: |
mkdir _deploy
tar xf dist/develop.tar.gz -C _deploy --strip-components=1
- name: Copy config
run: cp element.io/develop/config.json _deploy/config.json
- name: Populate 404.html - name: Build, Package & Upload sourcemaps
run: echo "404 Not Found" > _deploy/404.html run: "./scripts/ci_package.sh"
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: element
SENTRY_PROJECT: riot-web
# We only deploy the latest bundles to Cloudflare Pages and use _redirects to fallback to R2 for
# older ones. This redirect means that 'self' is insufficient in the CSP,
# and we have to add the R2 URL.
# Once Cloudflare redirects support proxying mode we will be able to ditch this.
# See Proxying in support table at https://developers.cloudflare.com/pages/platform/redirects
CSP_EXTRA_SOURCE: ${{ env.R2_PUBLIC_URL }}
- name: Populate _headers - run: mv dist/element-*.tar.gz dist/develop.tar.gz
run: cp .github/cfp_headers _deploy/_headers
# Redirect requests for the develop tarball and the historical bundles to R2 - uses: actions/upload-artifact@v3
- name: Populate _redirects with:
run: | name: webapp
{ path: dist/develop.tar.gz
echo "/develop.tar.gz $R2_PUBLIC_URL/develop.tar.gz 301" retention-days: 1
for bundle in $(aws s3 ls s3://$R2_BUCKET/bundles/ --endpoint-url $R2_URL --region=auto | awk '{print $2}'); do
echo "/bundles/${bundle}* $R2_PUBLIC_URL/bundles/${bundle}:splat 301"
done
} | tee _deploy/_redirects
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
- name: Wait for other steps to succeed - name: Extract webapp
uses: lewagon/wait-on-check-action@v1.2.0 run: |
with: mkdir _deploy
ref: ${{ github.ref }} tar xf dist/develop.tar.gz -C _deploy --strip-components=1
running-workflow-name: 'Build & Deploy develop.element.io'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-regexp: ^((?!SonarCloud|SonarQube|issues|board).)*$
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier - name: Copy config
# as the expires after 24h and requires auth to download. run: cp element.io/develop/config.json _deploy/config.json
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
- name: Deploy to R2 - name: Populate 404.html
run: | run: echo "404 Not Found" > _deploy/404.html
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto
aws s3 cp _deploy/bundles s3://$R2_BUCKET/bundles --recursive --endpoint-url $R2_URL --region=auto - name: Populate _headers
env: run: cp .github/cfp_headers _deploy/_headers
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }} # Redirect requests for the develop tarball and the historical bundles to R2
- name: Populate _redirects
- name: Deploy to Cloudflare Pages run: |
id: cfp {
uses: cloudflare/pages-action@1 echo "/develop.tar.gz $R2_PUBLIC_URL/develop.tar.gz 301"
with: for bundle in $(aws s3 ls s3://$R2_BUCKET/bundles/ --endpoint-url $R2_URL --region=auto | awk '{print $2}'); do
apiToken: ${{ secrets.CF_PAGES_TOKEN }} echo "/bundles/${bundle}* $R2_PUBLIC_URL/bundles/${bundle}:splat 301"
accountId: ${{ secrets.CF_PAGES_ACCOUNT_ID }} done
projectName: element-web-develop } | tee _deploy/_redirects
directory: _deploy env:
gitHubToken: ${{ secrets.GITHUB_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
- run: |
echo "Deployed to ${{ steps.cfp.outputs.url }}" >> $GITHUB_STEP_SUMMARY - name: Wait for other steps to succeed
uses: lewagon/wait-on-check-action@v1.2.0
with:
ref: ${{ github.ref }}
running-workflow-name: "Build & Deploy develop.element.io"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-regexp: ^((?!SonarCloud|SonarQube|issues|board).)*$
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
# as the expires after 24h and requires auth to download.
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
- name: Deploy to R2
run: |
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto
aws s3 cp _deploy/bundles s3://$R2_BUCKET/bundles --recursive --endpoint-url $R2_URL --region=auto
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
- name: Deploy to Cloudflare Pages
id: cfp
uses: cloudflare/pages-action@1
with:
apiToken: ${{ secrets.CF_PAGES_TOKEN }}
accountId: ${{ secrets.CF_PAGES_ACCOUNT_ID }}
projectName: element-web-develop
directory: _deploy
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
- run: |
echo "Deployed to ${{ steps.cfp.outputs.url }}" >> $GITHUB_STEP_SUMMARY

View File

@ -1,61 +1,61 @@
name: Dockerhub name: Dockerhub
on: on:
workflow_dispatch: { } workflow_dispatch: {}
push: push:
tags: [ v* ] tags: [v*]
schedule: schedule:
# This job can take a while, and we have usage limits, so just publish develop only twice a day # This job can take a while, and we have usage limits, so just publish develop only twice a day
- cron: '0 7/12 * * *' - cron: "0 7/12 * * *"
concurrency: ${{ github.ref_name }} concurrency: ${{ github.ref_name }}
jobs: jobs:
buildx: buildx:
name: Docker Buildx name: Docker Buildx
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: dockerhub environment: dockerhub
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
with: with:
install: true install: true
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: | images: |
vectorim/element-web vectorim/element-web
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=tag type=ref,event=tag
flavor: | flavor: |
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }} latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
- name: Update repo description - name: Update repo description
uses: peter-evans/dockerhub-description@v3 uses: peter-evans/dockerhub-description@v3
continue-on-error: true continue-on-error: true
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: vectorim/element-web repository: vectorim/element-web

View File

@ -2,155 +2,155 @@
# For all closed (completed) issues, cascade the closure onto any referenced rageshakes # For all closed (completed) issues, cascade the closure onto any referenced rageshakes
# For all closed (not planned) issues, comment on rageshakes to move them into the canonical issue if one exists # For all closed (not planned) issues, comment on rageshakes to move them into the canonical issue if one exists
on: on:
issues: issues:
types: [ closed ] types: [closed]
jobs: jobs:
tidy: tidy:
name: Tidy closed issues name: Tidy closed issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v6
id: main id: main
with: with:
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org) # PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
script: | script: |
const variables = { const variables = {
owner: context.repo.owner, owner: context.repo.owner,
name: context.repo.repo, name: context.repo.repo,
number: context.issue.number, number: context.issue.number,
}; };
const query = `query($owner:String!, $name:String!, $number:Int!) { const query = `query($owner:String!, $name:String!, $number:Int!) {
repository(owner: $owner, name: $name) { repository(owner: $owner, name: $name) {
issue(number: $number) { issue(number: $number) {
stateReason, stateReason,
timelineItems(first: 100, itemTypes: [MARKED_AS_DUPLICATE_EVENT, UNMARKED_AS_DUPLICATE_EVENT, CROSS_REFERENCED_EVENT]) { timelineItems(first: 100, itemTypes: [MARKED_AS_DUPLICATE_EVENT, UNMARKED_AS_DUPLICATE_EVENT, CROSS_REFERENCED_EVENT]) {
edges { edges {
node { node {
__typename __typename
... on MarkedAsDuplicateEvent { ... on MarkedAsDuplicateEvent {
canonical { canonical {
... on Issue { ... on Issue {
repository { repository {
nameWithOwner nameWithOwner
}
number
}
... on PullRequest {
repository {
nameWithOwner
}
number
}
}
}
... on UnmarkedAsDuplicateEvent {
canonical {
... on Issue {
repository {
nameWithOwner
}
number
}
... on PullRequest {
repository {
nameWithOwner
}
number
}
}
}
... on CrossReferencedEvent {
source {
... on Issue {
repository {
nameWithOwner
}
number
}
... on PullRequest {
repository {
nameWithOwner
}
number
}
}
}
}
} }
number
}
... on PullRequest {
repository {
nameWithOwner
}
number
} }
} }
} }
... on UnmarkedAsDuplicateEvent { }`;
canonical {
... on Issue { const result = await github.graphql(query, variables);
repository { const { stateReason, timelineItems: { edges } } = result.repository.issue;
nameWithOwner
} const RAGESHAKE_OWNER = "matrix-org";
number const RAGESHAKE_REPO = "element-web-rageshakes";
const rageshakes = new Set();
const duplicateOf = new Set();
console.log("Edges: ", JSON.stringify(edges));
for (const { node } of edges) {
switch(node.__typename) {
case "MarkedAsDuplicateEvent":
duplicateOf.add(node.canonical.repository.nameWithOwner + "#" + node.canonical.number);
break;
case "UnmarkedAsDuplicateEvent":
duplicateOf.remove(node.canonical.repository.nameWithOwner + "#" + node.canonical.number);
break;
case "CrossReferencedEvent":
if (node.source.repository.nameWithOwner === (RAGESHAKE_OWNER + "/" + RAGESHAKE_REPO)) {
rageshakes.add(node.source.number);
} }
... on PullRequest { break;
repository {
nameWithOwner
}
number
}
}
}
... on CrossReferencedEvent {
source {
... on Issue {
repository {
nameWithOwner
}
number
}
... on PullRequest {
repository {
nameWithOwner
}
number
}
}
} }
} }
}
}
}
}
}`;
const result = await github.graphql(query, variables); console.log("Duplicate of: ", duplicateOf);
const { stateReason, timelineItems: { edges } } = result.repository.issue; console.log("Found rageshakes: ", rageshakes);
const RAGESHAKE_OWNER = "matrix-org"; if (duplicateOf.size) {
const RAGESHAKE_REPO = "element-web-rageshakes"; const body = Array.from(duplicateOf).join("\n");
const rageshakes = new Set();
const duplicateOf = new Set();
console.log("Edges: ", JSON.stringify(edges)); // Comment on all rageshakes to create relationship to the issue this was closed as duplicate of
for (const rageshake of rageshakes) {
github.rest.issues.createComment({
owner: RAGESHAKE_OWNER,
repo: RAGESHAKE_REPO,
issue_number: rageshake,
body,
});
}
for (const { node } of edges) { // Duplicate was closed with wrong reason, fix it
switch(node.__typename) { if (stateReason === "COMPLETED") {
case "MarkedAsDuplicateEvent": core.setOutput("closeAsNotPlanned", "true");
duplicateOf.add(node.canonical.repository.nameWithOwner + "#" + node.canonical.number); }
break; } else {
case "UnmarkedAsDuplicateEvent": // This issue was closed, close all related rageshakes
duplicateOf.remove(node.canonical.repository.nameWithOwner + "#" + node.canonical.number); for (const rageshake of rageshakes) {
break; github.rest.issues.update({
case "CrossReferencedEvent": owner: RAGESHAKE_OWNER,
if (node.source.repository.nameWithOwner === (RAGESHAKE_OWNER + "/" + RAGESHAKE_REPO)) { repo: RAGESHAKE_REPO,
rageshakes.add(node.source.number); issue_number: rageshake,
} state: "closed",
break; });
} }
} }
- uses: actions/github-script@v6
console.log("Duplicate of: ", duplicateOf); name: Close duplicate as Not Planned
console.log("Found rageshakes: ", rageshakes); if: steps.main.outputs.closeAsNotPlanned
with:
if (duplicateOf.size) { # We do this step separately, and with the default token so as to not re-trigger this workflow when re-closing
const body = Array.from(duplicateOf).join("\n"); script: |
await github.graphql(`mutation($id:ID!) {
// Comment on all rageshakes to create relationship to the issue this was closed as duplicate of closeIssue(input: { issueId:$id, stateReason:NOT_PLANNED }) {
for (const rageshake of rageshakes) { clientMutationId
github.rest.issues.createComment({ }
owner: RAGESHAKE_OWNER, }`, {
repo: RAGESHAKE_REPO, id: context.payload.issue.node_id,
issue_number: rageshake, });
body,
});
}
// Duplicate was closed with wrong reason, fix it
if (stateReason === "COMPLETED") {
core.setOutput("closeAsNotPlanned", "true");
}
} else {
// This issue was closed, close all related rageshakes
for (const rageshake of rageshakes) {
github.rest.issues.update({
owner: RAGESHAKE_OWNER,
repo: RAGESHAKE_REPO,
issue_number: rageshake,
state: "closed",
});
}
}
- uses: actions/github-script@v6
name: Close duplicate as Not Planned
if: steps.main.outputs.closeAsNotPlanned
with:
# We do this step separately, and with the default token so as to not re-trigger this workflow when re-closing
script: |
await github.graphql(`mutation($id:ID!) {
closeIssue(input: { issueId:$id, stateReason:NOT_PLANNED }) {
clientMutationId
}
}`, {
id: context.payload.issue.node_id,
});

View File

@ -1,12 +1,12 @@
name: Pull Request name: Pull Request
on: on:
pull_request_target: pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ] types: [opened, edited, labeled, unlabeled, synchronize]
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }} concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs: jobs:
action: action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
with: with:
labels: "T-Defect,T-Enhancement,T-Task" labels: "T-Defect,T-Enhancement,T-Task"
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,15 +1,15 @@
name: SonarQube name: SonarQube
on: on:
workflow_run: workflow_run:
workflows: [ "Tests" ] workflows: ["Tests"]
types: types:
- completed - completed
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
sonarqube: sonarqube:
name: 🩻 SonarQube name: 🩻 SonarQube
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets: secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@ -1,119 +1,119 @@
name: Static Analysis name: Static Analysis
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
repository_dispatch: repository_dispatch:
types: [ element-web-notify ] types: [element-web-notify]
env: env:
# These must be set for fetchdep.sh to get the right branch # These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
ts_lint: ts_lint:
name: "Typescript Syntax Check" name: "Typescript Syntax Check"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Dependencies - name: Install Dependencies
run: "./scripts/layered.sh" run: "./scripts/layered.sh"
- name: Typecheck - name: Typecheck
run: "yarn run lint:types" run: "yarn run lint:types"
tsc-strict: tsc-strict:
name: Typescript Strict Error Checker name: Typescript Strict Error Checker
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
pull-requests: read pull-requests: read
checks: write checks: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install Deps - name: Install Deps
run: "scripts/layered.sh" run: "scripts/layered.sh"
- name: Get diff lines - name: Get diff lines
id: diff id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0 uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with: with:
include: '["\\.tsx?$"]' include: '["\\.tsx?$"]'
- name: Detecting files changed - name: Detecting files changed
id: files id: files
uses: futuratrepadeira/changed-files@v4.0.0 uses: futuratrepadeira/changed-files@v4.0.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$' pattern: '^.*\.tsx?$'
- uses: t3chguy/typescript-check-action@main - uses: t3chguy/typescript-check-action@main
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false use-check: false
check-fail-mode: added check-fail-mode: added
output-behaviour: annotate output-behaviour: annotate
ts-extra-args: '--strict --noImplicitAny' ts-extra-args: "--strict --noImplicitAny"
files-changed: ${{ steps.files.outputs.files_updated }} files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }} files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }} files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }} line-numbers: ${{ steps.diff.outputs.lineNumbers }}
i18n_lint: i18n_lint:
name: "i18n Check" name: "i18n Check"
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
js_lint: js_lint:
name: "ESLint" name: "ESLint"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
# Does not need branch matching as only analyses this layer # Does not need branch matching as only analyses this layer
- name: Install Deps - name: Install Deps
run: "yarn install --pure-lockfile" run: "yarn install --pure-lockfile"
- name: Run Linter - name: Run Linter
run: "yarn run lint:js" run: "yarn run lint:js"
style_lint: style_lint:
name: "Style Lint" name: "Style Lint"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
# Needs branch matching as it inherits .stylelintrc.js from matrix-react-sdk # Needs branch matching as it inherits .stylelintrc.js from matrix-react-sdk
- name: Install Dependencies - name: Install Dependencies
run: "./scripts/layered.sh" run: "./scripts/layered.sh"
- name: Run Linter - name: Run Linter
run: "yarn run lint:style" run: "yarn run lint:style"
analyse_dead_code: analyse_dead_code:
name: "Analyse Dead Code" name: "Analyse Dead Code"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Deps - name: Install Deps
run: "scripts/layered.sh" run: "scripts/layered.sh"
- name: Dead Code Analysis - name: Dead Code Analysis
run: "yarn run analyse:unused-exports" run: "yarn run analyse:unused-exports"

View File

@ -1,41 +1,41 @@
name: Tests name: Tests
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
repository_dispatch: repository_dispatch:
types: [ element-web-notify ] types: [element-web-notify]
env: env:
# These must be set for fetchdep.sh to get the right branch # These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
jest: jest:
name: Jest name: Jest
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Yarn cache - name: Yarn cache
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Dependencies - name: Install Dependencies
run: "./scripts/layered.sh" run: "./scripts/layered.sh"
- name: Get number of CPU cores - name: Get number of CPU cores
id: cpu-cores id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1 uses: SimenB/github-actions-cpu-cores@v1
- name: Run tests with coverage - name: Run tests with coverage
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}" run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: coverage name: coverage
path: | path: |
coverage coverage
!coverage/lcov-report !coverage/lcov-report

View File

@ -1,18 +1,18 @@
name: Move issued assigned to specific team members to their boards name: Move issued assigned to specific team members to their boards
on: on:
issues: issues:
types: [ assigned ] types: [assigned]
jobs: jobs:
web-app-team: web-app-team:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: |
contains(github.event.issue.assignees.*.login, 't3chguy') || contains(github.event.issue.assignees.*.login, 't3chguy') ||
contains(github.event.issue.assignees.*.login, 'turt2live') contains(github.event.issue.assignees.*.login, 'turt2live')
steps: steps:
- uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d
with: with:
project: Web App Team project: Web App Team
column: "In Progress" column: "In Progress"
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,15 +1,15 @@
name: Move new issues into Issue triage board name: Move new issues into Issue triage board
on: on:
issues: issues:
types: [ opened ] types: [opened]
jobs: jobs:
automate-project-columns: automate-project-columns:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d
with: with:
project: Issue triage project: Issue triage
column: Incoming column: Incoming
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,359 +1,359 @@
name: Move labelled issues to correct projects name: Move labelled issues to correct projects
on: on:
issues: issues:
types: [labeled] types: [labeled]
jobs: jobs:
apply_Z-Labs_label: apply_Z-Labs_label:
name: Add Z-Labs label for features behind labs flags name: Add Z-Labs label for features behind labs flags
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'A-Maths') || contains(github.event.issue.labels.*.name, 'A-Maths') ||
contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || contains(github.event.issue.labels.*.name, 'A-Message-Pinning') ||
contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || contains(github.event.issue.labels.*.name, 'A-Location-Sharing') ||
contains(github.event.issue.labels.*.name, 'Z-IA') || contains(github.event.issue.labels.*.name, 'Z-IA') ||
contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || contains(github.event.issue.labels.*.name, 'A-Themes-Custom') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
contains(github.event.issue.labels.*.name, 'A-Tags') || contains(github.event.issue.labels.*.name, 'A-Tags') ||
contains(github.event.issue.labels.*.name, 'A-Video-Rooms') || contains(github.event.issue.labels.*.name, 'A-Video-Rooms') ||
contains(github.event.issue.labels.*.name, 'A-Message-Starring') || contains(github.event.issue.labels.*.name, 'A-Message-Starring') ||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') || contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
contains(github.event.issue.labels.*.name, 'A-Element-Call') contains(github.event.issue.labels.*.name, 'A-Element-Call')
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v6
with: with:
script: | script: |
github.rest.issues.addLabels({ github.rest.issues.addLabels({
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
labels: ['Z-Labs'] labels: ['Z-Labs']
}) })
apply_Help-Wanted_label: apply_Help-Wanted_label:
name: Add "Help Wanted" label to all "good first issue" and Hacktoberfest name: Add "Help Wanted" label to all "good first issue" and Hacktoberfest
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'good first issue') || contains(github.event.issue.labels.*.name, 'good first issue') ||
contains(github.event.issue.labels.*.name, 'Hacktoberfest') contains(github.event.issue.labels.*.name, 'Hacktoberfest')
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v6
with: with:
script: | script: |
github.rest.issues.addLabels({ github.rest.issues.addLabels({
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
labels: ['Help Wanted'] labels: ['Help Wanted']
}) })
move_needs_info_issues: move_needs_info_issues:
name: X-Needs-Info issues to Need info column on triage board name: X-Needs-Info issues to Need info column on triage board
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: konradpabjan/move-labeled-or-milestoned-issue@190352295fe309fcb113b49193bc81d9aaa9cb01 - uses: konradpabjan/move-labeled-or-milestoned-issue@190352295fe309fcb113b49193bc81d9aaa9cb01
with: with:
action-token: "${{ secrets.ELEMENT_BOT_TOKEN }}" action-token: "${{ secrets.ELEMENT_BOT_TOKEN }}"
project-url: "https://github.com/vector-im/element-web/projects/27" project-url: "https://github.com/vector-im/element-web/projects/27"
column-name: "Need info" column-name: "Need info"
label-name: "X-Needs-Info" label-name: "X-Needs-Info"
add_priority_design_issues_to_project: add_priority_design_issues_to_project:
name: P1 X-Needs-Design to Design project board name: P1 X-Needs-Design to Design project board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Design') && contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
(contains(github.event.issue.labels.*.name, 'S-Critical') && (contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') || (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) || contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') && contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y')) contains(github.event.issue.labels.*.name, 'A11y'))
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
id: add_to_project id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }} contentid: ${{ github.event.issue.node_id }}
env: env:
PROJECT_ID: "PVT_kwDOAM0swc0sUA" PROJECT_ID: "PVT_kwDOAM0swc0sUA"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
add_product_issues: add_product_issues:
name: X-Needs-Product to product project board name: X-Needs-Product to product project board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'X-Needs-Product') contains(github.event.issue.labels.*.name, 'X-Needs-Product')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
id: add_to_project id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }} contentid: ${{ github.event.issue.node_id }}
env: env:
PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" PROJECT_ID: "PVT_kwDOAM0swc4AAg6N"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
Delight_issues_to_board: Delight_issues_to_board:
name: Delight issues to project board name: Delight issues to project board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'A-New-Search-Experience') || contains(github.event.issue.labels.*.name, 'A-New-Search-Experience') ||
(contains(github.event.issue.labels.*.name, 'A-Threads') && (contains(github.event.issue.labels.*.name, 'A-Threads') &&
(contains(github.event.issue.labels.*.name, 'S-Major') || (contains(github.event.issue.labels.*.name, 'S-Major') ||
contains(github.event.issue.labels.*.name, 'S-Critical'))) || contains(github.event.issue.labels.*.name, 'S-Critical'))) ||
contains(github.event.issue.labels.*.name, 'Team: Delight') || contains(github.event.issue.labels.*.name, 'Team: Delight') ||
contains(github.event.issue.labels.*.name, 'Z-NewUserJourney') contains(github.event.issue.labels.*.name, 'Z-NewUserJourney')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }} contentid: ${{ github.event.issue.node_id }}
env: env:
PROJECT_ID: "PVT_kwDOAM0swc1HvQ" PROJECT_ID: "PVT_kwDOAM0swc1HvQ"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
Search_issues_to_board:
name: Search issues to project board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'A-New-Search-Experience')
steps:
- uses: octokit/graphql-action@v2.x
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4ADtaO"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_voice-message_issues: Search_issues_to_board:
name: A-Voice Messages to voice message board name: Search issues to project board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'A-Voice Messages') contains(github.event.issue.labels.*.name, 'A-New-Search-Experience')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }} contentid: ${{ github.event.issue.node_id }}
env: env:
PROJECT_ID: "PVT_kwDOAM0swc2KCw" PROJECT_ID: "PVT_kwDOAM0swc4ADtaO"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_message_bubbles_issues: move_voice-message_issues:
name: A-Message-Bubbles to Message bubbles board name: A-Voice Messages to voice message board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') contains(github.event.issue.labels.*.name, 'A-Voice Messages')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }} contentid: ${{ github.event.issue.node_id }}
env: env:
PROJECT_ID: "PVT_kwDOAM0swc3m-g" PROJECT_ID: "PVT_kwDOAM0swc2KCw"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_ftue_issues: move_message_bubbles_issues:
name: Z-FTUE issues to the FTUE project board name: A-Message-Bubbles to Message bubbles board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'Z-FTUE') contains(github.event.issue.labels.*.name, 'A-Message-Bubbles')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }} contentid: ${{ github.event.issue.node_id }}
env: env:
PROJECT_ID: "PVT_kwDOAM0swc4AAqVx" PROJECT_ID: "PVT_kwDOAM0swc3m-g"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
move_WTF_issues: move_ftue_issues:
name: Z-WTF issues to the WTF project board name: Z-FTUE issues to the FTUE project board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'Z-WTF') contains(github.event.issue.labels.*.name, 'Z-FTUE')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }} contentid: ${{ github.event.issue.node_id }}
env: env:
PROJECT_ID: "PVT_kwDOAM0swc4AArk0" PROJECT_ID: "PVT_kwDOAM0swc4AAqVx"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
ps_features1: move_WTF_issues:
name: Add labelled issues to PS features team 1 name: Z-WTF issues to the WTF project board
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'A-Polls') || contains(github.event.issue.labels.*.name, 'Z-WTF')
contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || steps:
(contains(github.event.issue.labels.*.name, 'A-Voice-Messages') && - uses: octokit/graphql-action@v2.x
!contains(github.event.issue.labels.*.name, 'A-Broadcast')) || with:
(contains(github.event.issue.labels.*.name, 'A-Session-Mgmt') && headers: '{"GraphQL-Features": "projects_next_graphql"}'
contains(github.event.issue.labels.*.name, 'A-User-Settings')) query: |
steps: mutation add_to_project($projectid:ID!,$contentid:ID!) {
- uses: octokit/graphql-action@v2.x addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
id: add_to_project item {
with: id
headers: '{"GraphQL-Features": "projects_next_graphql"}' }
query: | }
mutation add_to_project($projectid:ID!,$contentid:ID!) { }
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { projectid: ${{ env.PROJECT_ID }}
item { contentid: ${{ github.event.issue.node_id }}
id env:
} PROJECT_ID: "PVT_kwDOAM0swc4AArk0"
} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AHJKF"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
ps_features2: ps_features1:
name: Add labelled issues to PS features team 2 name: Add labelled issues to PS features team 1
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'A-DM-Start') || contains(github.event.issue.labels.*.name, 'A-Polls') ||
contains(github.event.issue.labels.*.name, 'A-Broadcast') contains(github.event.issue.labels.*.name, 'A-Location-Sharing') ||
steps: (contains(github.event.issue.labels.*.name, 'A-Voice-Messages') &&
- uses: octokit/graphql-action@v2.x !contains(github.event.issue.labels.*.name, 'A-Broadcast')) ||
id: add_to_project (contains(github.event.issue.labels.*.name, 'A-Session-Mgmt') &&
with: contains(github.event.issue.labels.*.name, 'A-User-Settings'))
headers: '{"GraphQL-Features": "projects_next_graphql"}' steps:
query: | - uses: octokit/graphql-action@v2.x
mutation add_to_project($projectid:ID!,$contentid:ID!) { id: add_to_project
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { with:
item { headers: '{"GraphQL-Features": "projects_next_graphql"}'
id query: |
} mutation add_to_project($projectid:ID!,$contentid:ID!) {
} addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
} item {
projectid: ${{ env.PROJECT_ID }} id
contentid: ${{ github.event.issue.node_id }} }
env: }
PROJECT_ID: "PVT_kwDOAM0swc4AHJKd" }
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4AHJKF"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
ps_features3: ps_features2:
name: Add labelled issues to PS features team 3 name: Add labelled issues to PS features team 2
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') contains(github.event.issue.labels.*.name, 'A-DM-Start') ||
steps: contains(github.event.issue.labels.*.name, 'A-Broadcast')
- uses: octokit/graphql-action@v2.x steps:
id: add_to_project - uses: octokit/graphql-action@v2.x
with: id: add_to_project
headers: '{"GraphQL-Features": "projects_next_graphql"}' with:
query: | headers: '{"GraphQL-Features": "projects_next_graphql"}'
mutation add_to_project($projectid:ID!,$contentid:ID!) { query: |
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
item { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
id item {
} id
} }
} }
projectid: ${{ env.PROJECT_ID }} }
contentid: ${{ github.event.issue.node_id }} projectid: ${{ env.PROJECT_ID }}
env: contentid: ${{ github.event.issue.node_id }}
PROJECT_ID: "PVT_kwDOAM0swc4AHJKW" env:
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} PROJECT_ID: "PVT_kwDOAM0swc4AHJKd"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
voip: ps_features3:
name: Add labelled issues to VoIP project board name: Add labelled issues to PS features team 3
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'Team: VoIP') contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor')
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
id: add_to_project id: add_to_project
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) { mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }} contentid: ${{ github.event.issue.node_id }}
env: env:
PROJECT_ID: "PVT_kwDOAM0swc4ABMIk" PROJECT_ID: "PVT_kwDOAM0swc4AHJKW"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
voip:
name: Add labelled issues to VoIP project board
runs-on: ubuntu-latest
if: >
contains(github.event.issue.labels.*.name, 'Team: VoIP')
steps:
- uses: octokit/graphql-action@v2.x
id: add_to_project
with:
headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: |
mutation add_to_project($projectid:ID!,$contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item {
id
}
}
}
projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.issue.node_id }}
env:
PROJECT_ID: "PVT_kwDOAM0swc4ABMIk"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,139 +1,139 @@
name: Move pull requests asking for review to the relevant project name: Move pull requests asking for review to the relevant project
on: on:
pull_request_target: pull_request_target:
types: [ review_requested ] types: [review_requested]
jobs: jobs:
add_design_pr_to_project: add_design_pr_to_project:
name: Move PRs asking for design review to the design board name: Move PRs asking for design review to the design board
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
id: find_team_members id: find_team_members
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
query find_team_members($team: String!) { query find_team_members($team: String!) {
organization(login: "vector-im") { organization(login: "vector-im") {
team(slug: $team) { team(slug: $team) {
members { members {
nodes { nodes {
login login
} }
} }
} }
} }
} }
team: ${{ env.TEAM }} team: ${{ env.TEAM }}
env: env:
TEAM: "design" TEAM: "design"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- id: any_matching_reviewers - id: any_matching_reviewers
run: | run: |
# Fetch requested reviewers, and people who are on the team # Fetch requested reviewers, and people who are on the team
echo '${{ tojson(fromjson(steps.find_team_members.outputs.data).organization.team.members.nodes[*].login) }}' | tee /tmp/team_members.json echo '${{ tojson(fromjson(steps.find_team_members.outputs.data).organization.team.members.nodes[*].login) }}' | tee /tmp/team_members.json
echo '${{ tojson(github.event.pull_request.requested_reviewers[*].login) }}' | tee /tmp/reviewers.json echo '${{ tojson(github.event.pull_request.requested_reviewers[*].login) }}' | tee /tmp/reviewers.json
jq --raw-output .[] < /tmp/team_members.json | sort | tee /tmp/team_members.txt jq --raw-output .[] < /tmp/team_members.json | sort | tee /tmp/team_members.txt
jq --raw-output .[] < /tmp/reviewers.json | sort | tee /tmp/reviewers.txt jq --raw-output .[] < /tmp/reviewers.json | sort | tee /tmp/reviewers.txt
# Fetch requested team reviewers, and the name of the team # Fetch requested team reviewers, and the name of the team
echo '${{ tojson(github.event.pull_request.requested_teams[*].slug) }}' | tee /tmp/team_reviewers.json echo '${{ tojson(github.event.pull_request.requested_teams[*].slug) }}' | tee /tmp/team_reviewers.json
jq --raw-output .[] < /tmp/team_reviewers.json | sort | tee /tmp/team_reviewers.txt jq --raw-output .[] < /tmp/team_reviewers.json | sort | tee /tmp/team_reviewers.txt
echo '${{ env.TEAM }}' | tee /tmp/team.txt echo '${{ env.TEAM }}' | tee /tmp/team.txt
# If either a reviewer matches a team member, or a team matches our team, say "true" # If either a reviewer matches a team member, or a team matches our team, say "true"
if [ $(join /tmp/team_members.txt /tmp/reviewers.txt | wc -l) != 0 ]; then if [ $(join /tmp/team_members.txt /tmp/reviewers.txt | wc -l) != 0 ]; then
echo "::set-output name=match::true" echo "::set-output name=match::true"
elif [ $(join /tmp/team.txt /tmp/team_reviewers.txt | wc -l) != 0 ]; then elif [ $(join /tmp/team.txt /tmp/team_reviewers.txt | wc -l) != 0 ]; then
echo "::set-output name=match::true" echo "::set-output name=match::true"
else else
echo "::set-output name=match::false" echo "::set-output name=match::false"
fi fi
env: env:
TEAM: "design" TEAM: "design"
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
id: add_to_project id: add_to_project
if: steps.any_matching_reviewers.outputs.match == 'true' if: steps.any_matching_reviewers.outputs.match == 'true'
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!, $contentid:ID!) { mutation add_to_project($projectid:ID!, $contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.pull_request.node_id }} contentid: ${{ github.event.pull_request.node_id }}
env: env:
PROJECT_ID: "PN_kwDOAM0swc0sUA" PROJECT_ID: "PN_kwDOAM0swc0sUA"
TEAM: "design" TEAM: "design"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
add_product_pr_to_project: add_product_pr_to_project:
name: Move PRs asking for design review to the design board name: Move PRs asking for design review to the design board
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
id: find_team_members id: find_team_members
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
query find_team_members($team: String!) { query find_team_members($team: String!) {
organization(login: "vector-im") { organization(login: "vector-im") {
team(slug: $team) { team(slug: $team) {
members { members {
nodes { nodes {
login login
} }
} }
} }
} }
} }
team: ${{ env.TEAM }} team: ${{ env.TEAM }}
env: env:
TEAM: "product" TEAM: "product"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- id: any_matching_reviewers - id: any_matching_reviewers
run: | run: |
# Fetch requested reviewers, and people who are on the team # Fetch requested reviewers, and people who are on the team
echo '${{ tojson(fromjson(steps.find_team_members.outputs.data).organization.team.members.nodes[*].login) }}' | tee /tmp/team_members.json echo '${{ tojson(fromjson(steps.find_team_members.outputs.data).organization.team.members.nodes[*].login) }}' | tee /tmp/team_members.json
echo '${{ tojson(github.event.pull_request.requested_reviewers[*].login) }}' | tee /tmp/reviewers.json echo '${{ tojson(github.event.pull_request.requested_reviewers[*].login) }}' | tee /tmp/reviewers.json
jq --raw-output .[] < /tmp/team_members.json | sort | tee /tmp/team_members.txt jq --raw-output .[] < /tmp/team_members.json | sort | tee /tmp/team_members.txt
jq --raw-output .[] < /tmp/reviewers.json | sort | tee /tmp/reviewers.txt jq --raw-output .[] < /tmp/reviewers.json | sort | tee /tmp/reviewers.txt
# Fetch requested team reviewers, and the name of the team # Fetch requested team reviewers, and the name of the team
echo '${{ tojson(github.event.pull_request.requested_teams[*].slug) }}' | tee /tmp/team_reviewers.json echo '${{ tojson(github.event.pull_request.requested_teams[*].slug) }}' | tee /tmp/team_reviewers.json
jq --raw-output .[] < /tmp/team_reviewers.json | sort | tee /tmp/team_reviewers.txt jq --raw-output .[] < /tmp/team_reviewers.json | sort | tee /tmp/team_reviewers.txt
echo '${{ env.TEAM }}' | tee /tmp/team.txt echo '${{ env.TEAM }}' | tee /tmp/team.txt
# If either a reviewer matches a team member, or a team matches our team, say "true" # If either a reviewer matches a team member, or a team matches our team, say "true"
if [ $(join /tmp/team_members.txt /tmp/reviewers.txt | wc -l) != 0 ]; then if [ $(join /tmp/team_members.txt /tmp/reviewers.txt | wc -l) != 0 ]; then
echo "::set-output name=match::true" echo "::set-output name=match::true"
elif [ $(join /tmp/team.txt /tmp/team_reviewers.txt | wc -l) != 0 ]; then elif [ $(join /tmp/team.txt /tmp/team_reviewers.txt | wc -l) != 0 ]; then
echo "::set-output name=match::true" echo "::set-output name=match::true"
else else
echo "::set-output name=match::false" echo "::set-output name=match::false"
fi fi
env: env:
TEAM: "product" TEAM: "product"
- uses: octokit/graphql-action@v2.x - uses: octokit/graphql-action@v2.x
id: add_to_project id: add_to_project
if: steps.any_matching_reviewers.outputs.match == 'true' if: steps.any_matching_reviewers.outputs.match == 'true'
with: with:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!, $contentid:ID!) { mutation add_to_project($projectid:ID!, $contentid:ID!) {
addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
item { item {
id id
} }
} }
} }
projectid: ${{ env.PROJECT_ID }} projectid: ${{ env.PROJECT_ID }}
contentid: ${{ github.event.pull_request.node_id }} contentid: ${{ github.event.pull_request.node_id }}
env: env:
PROJECT_ID: "PN_kwDOAM0swc4AAg6N" PROJECT_ID: "PN_kwDOAM0swc4AAg6N"
TEAM: "product" TEAM: "product"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,30 +1,30 @@
name: Move P1 bugs to boards name: Move P1 bugs to boards
on: on:
issues: issues:
types: [labeled, unlabeled] types: [labeled, unlabeled]
jobs: jobs:
P1_issues_to_crypto_team_workboard: P1_issues_to_crypto_team_workboard:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
contains(github.event.issue.labels.*.name, 'Z-UISI') || contains(github.event.issue.labels.*.name, 'Z-UISI') ||
(contains(github.event.issue.labels.*.name, 'A-E2EE') || (contains(github.event.issue.labels.*.name, 'A-E2EE') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') || contains(github.event.issue.labels.*.name, 'A-E2EE-Cross-Signing') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') || contains(github.event.issue.labels.*.name, 'A-E2EE-Key-Backup') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) && contains(github.event.issue.labels.*.name, 'A-E2EE-SAS-Verification')) &&
(contains(github.event.issue.labels.*.name, 'T-Defect') && (contains(github.event.issue.labels.*.name, 'T-Defect') &&
contains(github.event.issue.labels.*.name, 'S-Critical') && contains(github.event.issue.labels.*.name, 'S-Critical') &&
(contains(github.event.issue.labels.*.name, 'O-Frequent') || (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'O-Occasional')) || contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
contains(github.event.issue.labels.*.name, 'S-Major') && contains(github.event.issue.labels.*.name, 'S-Major') &&
contains(github.event.issue.labels.*.name, 'O-Frequent') || contains(github.event.issue.labels.*.name, 'O-Frequent') ||
contains(github.event.issue.labels.*.name, 'A11y') && contains(github.event.issue.labels.*.name, 'A11y') &&
contains(github.event.issue.labels.*.name, 'O-Frequent')) contains(github.event.issue.labels.*.name, 'O-Frequent'))
steps: steps:
- uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d - uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d
with: with:
project: Crypto Team project: Crypto Team
column: Ready column: Ready
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,70 +1,70 @@
name: Move unlabelled from needs info columns to triaged name: Move unlabelled from needs info columns to triaged
on: on:
issues: issues:
types: [ unlabeled ] types: [unlabeled]
jobs: jobs:
Move_Unabeled_Issue_On_Project_Board: Move_Unabeled_Issue_On_Project_Board:
name: Move no longer X-Needs-Info issues to Triaged name: Move no longer X-Needs-Info issues to Triaged
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
${{ ${{
!contains(github.event.issue.labels.*.name, 'X-Needs-Info') }} !contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
env: env:
BOARD_NAME: "Issue triage" BOARD_NAME: "Issue triage"
OWNER: ${{ github.repository_owner }} OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }} REPO: ${{ github.event.repository.name }}
ISSUE: ${{ github.event.issue.number }} ISSUE: ${{ github.event.issue.number }}
steps: steps:
- name: Check if issue is already in "${{ env.BOARD_NAME }}" - name: Check if issue is already in "${{ env.BOARD_NAME }}"
run: | run: |
json=$(curl -s -H 'Content-Type: application/json' -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -X POST -d '{"query": "query($issue: Int!, $owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { projectCards { nodes { project { name } isArchived } } } } } ", "variables" : "{ \"issue\": '${ISSUE}', \"owner\": \"'${OWNER}'\", \"repo\": \"'${REPO}'\" }" }' https://api.github.com/graphql) json=$(curl -s -H 'Content-Type: application/json' -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -X POST -d '{"query": "query($issue: Int!, $owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { projectCards { nodes { project { name } isArchived } } } } } ", "variables" : "{ \"issue\": '${ISSUE}', \"owner\": \"'${OWNER}'\", \"repo\": \"'${REPO}'\" }" }' https://api.github.com/graphql)
if echo $json | jq '.data.repository.issue.projectCards.nodes | length'; then if echo $json | jq '.data.repository.issue.projectCards.nodes | length'; then
if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].project.name') =~ "${BOARD_NAME}" ]]; then if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].project.name') =~ "${BOARD_NAME}" ]]; then
if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].isArchived') == 'true' ]]; then if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].isArchived') == 'true' ]]; then
echo "Issue is already in Project '$BOARD_NAME', but is archived - skipping workflow"; echo "Issue is already in Project '$BOARD_NAME', but is archived - skipping workflow";
echo "SKIP_ACTION=true" >> $GITHUB_ENV echo "SKIP_ACTION=true" >> $GITHUB_ENV
else else
echo "Issue is already in Project '$BOARD_NAME', proceeding"; echo "Issue is already in Project '$BOARD_NAME', proceeding";
echo "ALREADY_IN_BOARD=true" >> $GITHUB_ENV echo "ALREADY_IN_BOARD=true" >> $GITHUB_ENV
fi fi
else else
echo "Issue is not in project '$BOARD_NAME', cancelling this workflow" echo "Issue is not in project '$BOARD_NAME', cancelling this workflow"
echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV
fi fi
fi fi
- name: Move issue - name: Move issue
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d
if: ${{ env.ALREADY_IN_BOARD == 'true' && env.SKIP_ACTION != 'true' }} if: ${{ env.ALREADY_IN_BOARD == 'true' && env.SKIP_ACTION != 'true' }}
with: with:
project: Issue triage project: Issue triage
column: Triaged column: Triaged
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
remove_Z-Labs_label: remove_Z-Labs_label:
name: Remove Z-Labs label when features behind labs flags are removed name: Remove Z-Labs label when features behind labs flags are removed
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: > if: >
!(contains(github.event.issue.labels.*.name, 'A-Maths') || !(contains(github.event.issue.labels.*.name, 'A-Maths') ||
contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || contains(github.event.issue.labels.*.name, 'A-Message-Pinning') ||
contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || contains(github.event.issue.labels.*.name, 'A-Location-Sharing') ||
contains(github.event.issue.labels.*.name, 'Z-IA') || contains(github.event.issue.labels.*.name, 'Z-IA') ||
contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || contains(github.event.issue.labels.*.name, 'A-Themes-Custom') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
contains(github.event.issue.labels.*.name, 'A-Tags') || contains(github.event.issue.labels.*.name, 'A-Tags') ||
contains(github.event.issue.labels.*.name, 'A-Video-Rooms') || contains(github.event.issue.labels.*.name, 'A-Video-Rooms') ||
contains(github.event.issue.labels.*.name, 'A-Message-Starring') || contains(github.event.issue.labels.*.name, 'A-Message-Starring') ||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') || contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
contains(github.event.issue.labels.*.name, 'A-Element-Call')) && contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
contains(github.event.issue.labels.*.name, 'Z-Labs') contains(github.event.issue.labels.*.name, 'Z-Labs')
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v6
with: with:
script: | script: |
github.rest.issues.removeLabel({ github.rest.issues.removeLabel({
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
name: ['Z-Labs'] name: ['Z-Labs']
}) })

View File

@ -1,8 +1,8 @@
name: Upgrade Dependencies name: Upgrade Dependencies
on: on:
workflow_dispatch: { } workflow_dispatch: {}
jobs: jobs:
upgrade: upgrade:
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View File

@ -1,36 +1,36 @@
{ {
"minify": true, "minify": true,
"enableClasses": false, "enableClasses": false,
"feature-detects": [ "feature-detects": [
"test/css/animations", "test/css/animations",
"test/css/displaytable", "test/css/displaytable",
"test/css/filters", "test/css/filters",
"test/css/flexbox", "test/css/flexbox",
"test/css/objectfit", "test/css/objectfit",
"test/es5/date", "test/es5/date",
"test/es5/function", "test/es5/function",
"test/es5/object", "test/es5/object",
"test/es5/undefined", "test/es5/undefined",
"test/es6/array", "test/es6/array",
"test/es6/collections", "test/es6/collections",
"test/es6/promises", "test/es6/promises",
"test/es6/string", "test/es6/string",
"test/svg", "test/svg",
"test/svg/asimg", "test/svg/asimg",
"test/svg/filters", "test/svg/filters",
"test/url/parser", "test/url/parser",
"test/url/urlsearchparams", "test/url/urlsearchparams",
"test/cors", "test/cors",
"test/crypto", "test/crypto",
"test/iframe/sandbox", "test/iframe/sandbox",
"test/json", "test/json",
"test/network/fetch", "test/network/fetch",
"test/storage/localstorage", "test/storage/localstorage",
"test/window/resizeobserver" "test/window/resizeobserver"
] ]
} }

13115
CHANGELOG.md

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
Contributing code to Element Web # Contributing code to Element Web
================================
Everyone is welcome to contribute code to Element Web, provided that they are Everyone is welcome to contribute code to Element Web, provided that they are
willing to license their contributions under the same license as the project willing to license their contributions under the same license as the project
@ -9,8 +8,7 @@ license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)). [LICENSE](LICENSE)).
How to contribute ## How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes it on github, and then create a pull request to ask us to pull your changes
@ -20,29 +18,30 @@ We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves. ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description: Things that should go into your PR description:
* A changelog entry in the `Notes` section (see below)
* References to any bugs fixed by the change (in GitHub's `Fixes` notation) - A changelog entry in the `Notes` section (see below)
* Describe the why and what is changing in the PR description so it's easy for - References to any bugs fixed by the change (in GitHub's `Fixes` notation)
onlookers and reviewers to onboard and context switch. This information is - Describe the why and what is changing in the PR description so it's easy for
also helpful when we come back to look at this in 6 months and ask "why did onlookers and reviewers to onboard and context switch. This information is
we do it like that?" we have a chance of finding out. also helpful when we come back to look at this in 6 months and ask "why did
* Why didn't it work before? Why does it work now? What use cases does it we do it like that?" we have a chance of finding out.
- Why didn't it work before? Why does it work now? What use cases does it
unlock? unlock?
* If you find yourself adding information on how the code works or why you - If you find yourself adding information on how the code works or why you
chose to do it the way you did, make sure this information is instead chose to do it the way you did, make sure this information is instead
written as comments in the code itself. written as comments in the code itself.
* Sometimes a PR can change considerably as it is developed. In this case, - Sometimes a PR can change considerably as it is developed. In this case,
the description should be updated to reflect the most recent state of the description should be updated to reflect the most recent state of
the PR. (It can be helpful to retain the old content under a suitable the PR. (It can be helpful to retain the old content under a suitable
heading, for additional context.) heading, for additional context.)
* Include both **before** and **after** screenshots to easily compare and discuss - Include both **before** and **after** screenshots to easily compare and discuss
what's changing. what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the - Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change. code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand - Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it. why the change is necessary or how they might better understand and review it.
We rely on information in pull request to populate the information that goes into We rely on information in pull request to populate the information that goes into
the changelogs our users see, both for Element Web itself and other projects on the changelogs our users see, both for Element Web itself and other projects on
which it is based. This is picked up from both labels on the pull request and which it is based. This is picked up from both labels on the pull request and
the `Notes:` annotation in the description. By default, the PR title will be the `Notes:` annotation in the description. By default, the PR title will be
@ -50,8 +49,7 @@ used for the changelog entry, but you can specify more options, as follows.
To add a longer, more detailed description of the change for the changelog: To add a longer, more detailed description of the change for the changelog:
_Fix llama herding bug_
*Fix llama herding bug*
``` ```
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
@ -60,7 +58,8 @@ Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where th
For some PRs, it's not useful to have an entry in the user-facing changelog (this is For some PRs, it's not useful to have an entry in the user-facing changelog (this is
the default for PRs labelled with `T-Task`): the default for PRs labelled with `T-Task`):
*Remove outdated comment from `Ungulates.ts`* _Remove outdated comment from `Ungulates.ts`_
``` ```
Notes: none Notes: none
``` ```
@ -68,16 +67,18 @@ Notes: none
Sometimes, you're fixing a bug in a downstream project, in which case you want Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too: an entry in that project's changelog. You can do that too:
*Fix another herding bug* _Fix another herding bug_
``` ```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
``` ```
This example is for Element Web. You can specify: This example is for Element Web. You can specify:
* matrix-react-sdk
* element-web - matrix-react-sdk
* element-desktop - element-web
- element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need way, additionally adding the `X-Breaking-Change` label (see below). There's no need
@ -85,17 +86,18 @@ to specify in the notes that it's a breaking change - this will be added
automatically based on the label - but remember to tell the developer how to automatically based on the label - but remember to tell the developer how to
migrate: migrate:
*Remove legacy class* _Remove legacy class_
``` ```
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead. Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
``` ```
Other metadata can be added using labels. Other metadata can be added using labels.
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump. - `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump.
* `T-Defect`: A bug fix (in either code or docs). - `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump.
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one. - `T-Defect`: A bug fix (in either code or docs).
- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
If you don't have permission to add labels, your PR reviewer(s) can work with you If you don't have permission to add labels, your PR reviewer(s) can work with you
to add them: ask in the PR description or comments. to add them: ask in the PR description or comments.
@ -104,8 +106,8 @@ We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes. checks, so please check back after a few minutes.
Tests ## Tests
-----
Your PR should include tests. Your PR should include tests.
For new user facing features in `matrix-js-sdk`, `matrix-react-sdk` or `element-web`, you For new user facing features in `matrix-js-sdk`, `matrix-react-sdk` or `element-web`, you
@ -129,7 +131,7 @@ end-to-end test; which is best depends on what sort of test most concisely
exercises the area. exercises the area.
Changes to must be accompanied by unit tests written in Jest. Changes to must be accompanied by unit tests written in Jest.
These are located in `/spec/` in `matrix-js-sdk` or `/test/` in `element-web` These are located in `/spec/` in `matrix-js-sdk` or `/test/` in `element-web`
and `matrix-react-sdk`. and `matrix-react-sdk`.
When writing unit tests, please aim for a high level of test coverage When writing unit tests, please aim for a high level of test coverage
@ -139,6 +141,7 @@ why it's not possible in your PR.
Some sections of code are not sensible to add coverage for, such as those Some sections of code are not sensible to add coverage for, such as those
which explicitly inhibit noisy logging for tests. Which can be hidden using which explicitly inhibit noisy logging for tests. Which can be hidden using
an istanbul magic comment as [documented here][1]. See example: an istanbul magic comment as [documented here][1]. See example:
```javascript ```javascript
/* istanbul ignore if */ /* istanbul ignore if */
if (process.env.NODE_ENV !== "test") { if (process.env.NODE_ENV !== "test") {
@ -160,8 +163,8 @@ tests later will become progressively more difficult.
If you're not sure how to approach writing tests for your change, ask for help If you're not sure how to approach writing tests for your change, ask for help
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org). in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
Code style ## Code style
----------
Element Web aims to target TypeScript/ES6. All new files should be written in Element Web aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible. TypeScript and existing files should use ES6 principles where possible.
@ -174,11 +177,11 @@ The remaining code style is documented in [code_style.md](./code_style.md).
Contributors are encouraged to it and follow the principles set out there. Contributors are encouraged to it and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project, Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it and **_never_** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise. makes it horribly hard to review otherwise.
Attribution ## Attribution
-----------
Everyone who contributes anything to Matrix is welcome to be listed in the Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short change to AUTHORS.rst in your pull request to list yourself and a short
@ -187,8 +190,8 @@ give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :) and we'll try to fix it :)
Sign off ## Sign off
--------
In order to have a concrete record that your contribution is intentional In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel adopted the same lightweight approach that the Linux Kernel
@ -259,19 +262,16 @@ on Git 2.17+ you can mass signoff using rebase:
git rebase --signoff origin/develop git rebase --signoff origin/develop
``` ```
Review expectations # Review expectations
===================
See https://github.com/vector-im/element-meta/wiki/Review-process See https://github.com/vector-im/element-meta/wiki/Review-process
# Merge Strategy
Merge Strategy
==============
The preferred method for merging pull requests is squash merging to keep the The preferred method for merging pull requests is squash merging to keep the
commit history trim, but it is up to the discretion of the team member merging commit history trim, but it is up to the discretion of the team member merging
the change. We do not support rebase merges due to `allchange` being unable to the change. We do not support rebase merges due to `allchange` being unable to
handle them. When merging make sure to leave the default commit title, or handle them. When merging make sure to leave the default commit title, or
at least leave the PR number at the end in brackets like by default. at least leave the PR number at the end in brackets like by default.
When stacking pull requests, you may wish to do the following: When stacking pull requests, you may wish to do the following:
@ -279,5 +279,4 @@ When stacking pull requests, you may wish to do the following:
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR. 2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop. 3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md [1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md

133
README.md
View File

@ -7,37 +7,34 @@
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=element-web&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=element-web) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=element-web&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=element-web)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=element-web&metric=bugs)](https://sonarcloud.io/summary/new_code?id=element-web) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=element-web&metric=bugs)](https://sonarcloud.io/summary/new_code?id=element-web)
Element # Element
=======
Element (formerly known as Vector and Riot) is a Matrix web client built using the [Matrix Element (formerly known as Vector and Riot) is a Matrix web client built using the [Matrix
React SDK](https://github.com/matrix-org/matrix-react-sdk). React SDK](https://github.com/matrix-org/matrix-react-sdk).
Supported Environments # Supported Environments
======================
Element has several tiers of support for different environments: Element has several tiers of support for different environments:
* Supported - Supported
* Definition: Issues **actively triaged**, regressions **block** the release - Definition: Issues **actively triaged**, regressions **block** the release
* Last 2 major versions of Chrome, Firefox, Safari, and Edge on desktop OSes - Last 2 major versions of Chrome, Firefox, Safari, and Edge on desktop OSes
* Latest release of official Element Desktop app on desktop OSes - Latest release of official Element Desktop app on desktop OSes
* Desktop OSes means macOS, Windows, and Linux versions for desktop devices - Desktop OSes means macOS, Windows, and Linux versions for desktop devices
that are actively supported by the OS vendor and receive security updates that are actively supported by the OS vendor and receive security updates
* Experimental - Experimental
* Definition: Issues **accepted**, regressions **do not block** the release - Definition: Issues **accepted**, regressions **do not block** the release
* Element as an installed PWA via current stable version of Chrome, Firefox, and Safari - Element as an installed PWA via current stable version of Chrome, Firefox, and Safari
* Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS - Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS
* Not supported - Not supported
* Definition: Issues only affecting unsupported environments are **closed** - Definition: Issues only affecting unsupported environments are **closed**
* Everything else - Everything else
For accessing Element on an Android or iOS device, we currently recommend the For accessing Element on an Android or iOS device, we currently recommend the
native apps [element-android](https://github.com/vector-im/element-android) native apps [element-android](https://github.com/vector-im/element-android)
and [element-ios](https://github.com/vector-im/element-ios). and [element-ios](https://github.com/vector-im/element-ios).
Getting Started # Getting Started
===============
The easiest way to test Element is to just use the hosted copy at <https://app.element.io>. The easiest way to test Element is to just use the hosted copy at <https://app.element.io>.
The `develop` branch is continuously deployed to <https://develop.element.io> The `develop` branch is continuously deployed to <https://develop.element.io>
@ -67,47 +64,39 @@ and thus allowed.
To install Element as a desktop application, see [Running as a desktop To install Element as a desktop application, see [Running as a desktop
app](#running-as-a-desktop-app) below. app](#running-as-a-desktop-app) below.
Important Security Notes # Important Security Notes
========================
Separate domains ## Separate domains
----------------
We do not recommend running Element from the same domain name as your Matrix We do not recommend running Element from the same domain name as your Matrix
homeserver. The reason is the risk of XSS (cross-site-scripting) homeserver. The reason is the risk of XSS (cross-site-scripting)
vulnerabilities that could occur if someone caused Element to load and render vulnerabilities that could occur if someone caused Element to load and render
malicious user generated content from a Matrix API which then had trusted malicious user generated content from a Matrix API which then had trusted
access to Element (or other apps) due to sharing the same domain. access to Element (or other apps) due to sharing the same domain.
We have put some coarse mitigations into place to try to protect against this We have put some coarse mitigations into place to try to protect against this
situation, but it's still not good practice to do it in the first place. See situation, but it's still not good practice to do it in the first place. See
<https://github.com/vector-im/element-web/issues/1977> for more details. <https://github.com/vector-im/element-web/issues/1977> for more details.
Configuration best practices ## Configuration best practices
----------------------------
Unless you have special requirements, you will want to add the following to Unless you have special requirements, you will want to add the following to
your web server configuration when hosting Element Web: your web server configuration when hosting Element Web:
* The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being - The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being
framed and protect from [clickjacking][owasp-clickjacking]. framed and protect from [clickjacking][owasp-clickjacking].
* The `frame-ancestors 'none'` directive to your `Content-Security-Policy` - The `frame-ancestors 'none'` directive to your `Content-Security-Policy`
header, as the modern replacement for `X-Frame-Options` (though both should be header, as the modern replacement for `X-Frame-Options` (though both should be
included since not all browsers support it yet, see included since not all browsers support it yet, see
[this][owasp-clickjacking-csp]). [this][owasp-clickjacking-csp]).
* The `X-Content-Type-Options: nosniff` header, to [disable MIME - The `X-Content-Type-Options: nosniff` header, to [disable MIME
sniffing][mime-sniffing]. sniffing][mime-sniffing].
* The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in - The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in
legacy browsers. legacy browsers.
[mime-sniffing]: [mime-sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing> [owasp-clickjacking-csp]: https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#content-security-policy-frame-ancestors-examples
[owasp-clickjacking]: https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html
[owasp-clickjacking-csp]:
<https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#content-security-policy-frame-ancestors-examples>
[owasp-clickjacking]:
<https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html>
If you are using nginx, this would look something like the following: If you are using nginx, this would look something like the following:
@ -117,7 +106,9 @@ add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "frame-ancestors 'none'"; add_header Content-Security-Policy "frame-ancestors 'none'";
``` ```
For Apache, the configuration looks like: For Apache, the configuration looks like:
``` ```
Header set X-Frame-Options SAMEORIGIN Header set X-Frame-Options SAMEORIGIN
Header set X-Content-Type-Options nosniff Header set X-Content-Type-Options nosniff
@ -129,8 +120,7 @@ Note: In case you are already setting a `Content-Security-Policy` header
elsewhere, you should modify it to include the `frame-ancestors` directive elsewhere, you should modify it to include the `frame-ancestors` directive
instead of adding that last line. instead of adding that last line.
Building From Source # Building From Source
====================
Element is a modular webapp built with modern ES6 and uses a Node.js build system. Element is a modular webapp built with modern ES6 and uses a Node.js build system.
Ensure you have the latest LTS version of Node.js installed. Ensure you have the latest LTS version of Node.js installed.
@ -143,7 +133,7 @@ guide](https://classic.yarnpkg.com/en/docs/install) if you do not have it alread
1. Clone the repo: `git clone https://github.com/vector-im/element-web.git`. 1. Clone the repo: `git clone https://github.com/vector-im/element-web.git`.
1. Switch to the element-web directory: `cd element-web`. 1. Switch to the element-web directory: `cd element-web`.
1. Install the prerequisites: `yarn install`. 1. Install the prerequisites: `yarn install`.
* If you're using the `develop` branch, then it is recommended to set up a - If you're using the `develop` branch, then it is recommended to set up a
proper development environment (see [Setting up a dev proper development environment (see [Setting up a dev
environment](#setting-up-a-dev-environment) below). Alternatively, you environment](#setting-up-a-dev-environment) below). Alternatively, you
can use <https://develop.element.io> - the continuous integration release of can use <https://develop.element.io> - the continuous integration release of
@ -160,8 +150,7 @@ will not appear in Settings without using the dist script. You can then mount th
`webapp` directory on your web server to actually serve up the app, which is `webapp` directory on your web server to actually serve up the app, which is
entirely static content. entirely static content.
Running as a Desktop app # Running as a Desktop app
========================
Element can also be run as a desktop app, wrapped in Electron. You can download a Element can also be run as a desktop app, wrapped in Electron. You can download a
pre-built version from <https://element.io/get-started> or, if you prefer, pre-built version from <https://element.io/get-started> or, if you prefer,
@ -173,7 +162,7 @@ Many thanks to @aviraldg for the initial work on the Electron integration.
Other options for running as a desktop app: Other options for running as a desktop app:
* @asdf:matrix.org points out that you can use nativefier and it just works(tm) - @asdf:matrix.org points out that you can use nativefier and it just works(tm)
```bash ```bash
yarn global add nativefier yarn global add nativefier
@ -183,8 +172,7 @@ nativefier https://app.element.io/
The [configuration docs](docs/config.md#desktop-app-configuration) show how to The [configuration docs](docs/config.md#desktop-app-configuration) show how to
override the desktop app's default settings if desired. override the desktop app's default settings if desired.
Running from Docker # Running from Docker
===================
The Docker image can be used to serve element-web as a web server. The easiest way to use The Docker image can be used to serve element-web as a web server. The easiest way to use
it is to use the prebuilt image: it is to use the prebuilt image:
@ -223,26 +211,22 @@ docker build -t \
. .
``` ```
Running in Kubernetes # Running in Kubernetes
=====================
The provided element-web docker image can also be run from within a Kubernetes cluster. The provided element-web docker image can also be run from within a Kubernetes cluster.
See the [Kubernetes example](docs/kubernetes.md) for more details. See the [Kubernetes example](docs/kubernetes.md) for more details.
config.json # config.json
===========
Element supports a variety of settings to configure default servers, behaviour, themes, etc. Element supports a variety of settings to configure default servers, behaviour, themes, etc.
See the [configuration docs](docs/config.md) for more details. See the [configuration docs](docs/config.md) for more details.
Labs Features # Labs Features
=============
Some features of Element may be enabled by flags in the `Labs` section of the settings. Some features of Element may be enabled by flags in the `Labs` section of the settings.
Some of these features are described in [labs.md](https://github.com/vector-im/element-web/blob/develop/docs/labs.md). Some of these features are described in [labs.md](https://github.com/vector-im/element-web/blob/develop/docs/labs.md).
Caching requirements # Caching requirements
====================
Element requires the following URLs not to be cached, when/if you are serving Element from your own webserver: Element requires the following URLs not to be cached, when/if you are serving Element from your own webserver:
@ -259,8 +243,7 @@ webserver to return `Cache-Control: no-cache` for `/`. This ensures the browser
the next page load after it's been deployed. Note that this is already configured for you in the nginx config of our the next page load after it's been deployed. Note that this is already configured for you in the nginx config of our
Dockerfile. Dockerfile.
Development # Development
===========
Before attempting to develop on Element you **must** read the [developer guide Before attempting to develop on Element you **must** read the [developer guide
for `matrix-react-sdk`](https://github.com/matrix-org/matrix-react-sdk#developer-guide), which for `matrix-react-sdk`](https://github.com/matrix-org/matrix-react-sdk#developer-guide), which
@ -282,7 +265,7 @@ higher and lower level React components useful for building Matrix communication
apps using React. apps using React.
Please note that Element is intended to run correctly without access to the public Please note that Element is intended to run correctly without access to the public
internet. So please don't depend on resources (JS libs, CSS, images, fonts) internet. So please don't depend on resources (JS libs, CSS, images, fonts)
hosted by external CDNs or servers but instead please package all dependencies hosted by external CDNs or servers but instead please package all dependencies
into Element itself. into Element itself.
@ -290,8 +273,7 @@ CSS hot-reload is available as an opt-in development feature. You can enable it
by defining a `CSS_HOT_RELOAD` environment variable, in a `.env` file in the root by defining a `CSS_HOT_RELOAD` environment variable, in a `.env` file in the root
of the repository. See `.env.example` for documentation and an example. of the repository. See `.env.example` for documentation and an example.
Setting up a dev environment # Setting up a dev environment
============================
Much of the functionality in Element is actually in the `matrix-react-sdk` and Much of the functionality in Element is actually in the `matrix-react-sdk` and
`matrix-js-sdk` modules. It is possible to set these up in a way that makes it `matrix-js-sdk` modules. It is possible to set these up in a way that makes it
@ -300,7 +282,7 @@ having to manually rebuild each time.
First clone and build `matrix-js-sdk`: First clone and build `matrix-js-sdk`:
``` bash ```bash
git clone https://github.com/matrix-org/matrix-js-sdk.git git clone https://github.com/matrix-org/matrix-js-sdk.git
pushd matrix-js-sdk pushd matrix-js-sdk
yarn link yarn link
@ -347,9 +329,9 @@ Wait a few seconds for the initial build to finish; you should see something lik
[element-js] 「wdm」: Compiled successfully. [element-js] 「wdm」: Compiled successfully.
``` ```
Remember, the command will not terminate since it runs the web server Remember, the command will not terminate since it runs the web server
and rebuilds source files when they change. This development server also and rebuilds source files when they change. This development server also
disables caching, so do NOT use it in production. disables caching, so do NOT use it in production.
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element. Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
@ -377,7 +359,7 @@ echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p sudo sysctl -p
``` ```
___ ---
When you make changes to `matrix-react-sdk` or `matrix-js-sdk` they should be When you make changes to `matrix-react-sdk` or `matrix-js-sdk` they should be
automatically picked up by webpack and built. automatically picked up by webpack and built.
@ -386,8 +368,7 @@ If any of these steps error with, `file table overflow`, you are probably on a m
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again. which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
You'll need to do this in each new terminal you open before building Element. You'll need to do this in each new terminal you open before building Element.
Running the tests ## Running the tests
-----------------
There are a number of application-level tests in the `tests` directory; these There are a number of application-level tests in the `tests` directory; these
are designed to run with Jest and JSDOM. To run them are designed to run with Jest and JSDOM. To run them
@ -400,8 +381,7 @@ yarn test
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests. See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
Translations # Translations
============
To add a new translation, head to the [translating doc](docs/translating.md). To add a new translation, head to the [translating doc](docs/translating.md).
@ -409,8 +389,7 @@ For a developer guide, see the [translating dev doc](docs/translating-dev.md).
[<img src="https://translate.element.io/widgets/element-web/-/multi-auto.svg" alt="translationsstatus" width="340">](https://translate.element.io/engage/element-web/?utm_source=widget) [<img src="https://translate.element.io/widgets/element-web/-/multi-auto.svg" alt="translationsstatus" width="340">](https://translate.element.io/engage/element-web/?utm_source=widget)
Triaging issues # Triaging issues
===============
Issues are triaged by community members and the Web App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process). Issues are triaged by community members and the Web App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process).

View File

@ -1,18 +1,21 @@
module.exports = { module.exports = {
"sourceMaps": true, sourceMaps: true,
"presets": [ presets: [
["@babel/preset-env", { [
"targets": [ "@babel/preset-env",
"last 2 Chrome versions", {
"last 2 Firefox versions", targets: [
"last 2 Safari versions", "last 2 Chrome versions",
"last 2 Edge versions", "last 2 Firefox versions",
], "last 2 Safari versions",
}], "last 2 Edge versions",
],
},
],
"@babel/preset-typescript", "@babel/preset-typescript",
"@babel/preset-react", "@babel/preset-react",
], ],
"plugins": [ plugins: [
"@babel/plugin-proposal-export-default-from", "@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-class-properties",

View File

@ -3,10 +3,10 @@
This code style applies to projects which the element-web team directly maintains or is reasonably This code style applies to projects which the element-web team directly maintains or is reasonably
adjacent to. As of writing, these are: adjacent to. As of writing, these are:
* element-desktop - element-desktop
* element-web - element-web
* matrix-react-sdk - matrix-react-sdk
* matrix-js-sdk - matrix-js-sdk
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk Other projects might extend this code style for increased strictness. For example, matrix-events-sdk
has stricter code organization to reduce the maintenance burden. These projects will declare their code has stricter code organization to reduce the maintenance burden. These projects will declare their code
@ -66,37 +66,24 @@ Unless otherwise specified, the following applies to all code:
```typescript ```typescript
// Function arguments // Function arguments
function doThing( function doThing(arg1: string, arg2: string, arg3: string): boolean {
arg1: string, return !!arg1 && !!arg2 && !!arg3;
arg2: string,
arg3: string,
): boolean {
return !!arg1
&& !!arg2
&& !!arg3;
} }
// Calling a function // Calling a function
doThing( doThing("String 1", "String 2", "String 3");
"String 1",
"String 2",
"String 3",
);
// Reduce line verbosity when possible/reasonable // Reduce line verbosity when possible/reasonable
doThing( doThing("String1", "String 2", "A much longer string 3");
"String1", "String 2",
"A much longer string 3",
);
// Chaining function calls // Chaining function calls
something.doThing() something
.doThing()
.doOtherThing() .doOtherThing()
.doMore() .doMore()
.somethingElse(it => .somethingElse((it) => useIt(it));
useIt(it)
);
``` ```
4. Use semicolons for block/line termination. 4. Use semicolons for block/line termination.
1. Except when defining interfaces, classes, and non-arrow functions specifically. 1. Except when defining interfaces, classes, and non-arrow functions specifically.
5. When a statement's body is a single line, it may be written without curly braces, so long as the body is placed on 5. When a statement's body is a single line, it may be written without curly braces, so long as the body is placed on
@ -105,6 +92,7 @@ Unless otherwise specified, the following applies to all code:
```typescript ```typescript
if (x) doThing(); if (x) doThing();
``` ```
6. Blocks for `if`, `for`, `switch` and so on must have a space surrounding the condition, but not 6. Blocks for `if`, `for`, `switch` and so on must have a space surrounding the condition, but not
within the condition. within the condition.
@ -113,11 +101,13 @@ Unless otherwise specified, the following applies to all code:
doThing(); doThing();
} }
``` ```
7. Mixing of logical operands requires brackets to explicitly define boolean logic. 7. Mixing of logical operands requires brackets to explicitly define boolean logic.
```typescript ```typescript
if ((a > b && b > c) || (d < e)) return true; if ((a > b && b > c) || d < e) return true;
``` ```
8. Ternaries use the same rules as `if` statements, plus the following: 8. Ternaries use the same rules as `if` statements, plus the following:
```typescript ```typescript
@ -125,14 +115,13 @@ Unless otherwise specified, the following applies to all code:
const val = a > b ? doThing() : doOtherThing(); const val = a > b ? doThing() : doOtherThing();
// Multiline is also okay // Multiline is also okay
const val = a > b const val = a > b ? doThing() : doOtherThing();
? doThing()
: doOtherThing();
// Use brackets when using multiple conditions. // Use brackets when using multiple conditions.
// Maximum 3 conditions, prefer 2 or less. // Maximum 3 conditions, prefer 2 or less.
const val = (a > b && b > c) ? doThing() : doOtherThing(); const val = a > b && b > c ? doThing() : doOtherThing();
``` ```
9. lowerCamelCase is used for function and variable naming. 9. lowerCamelCase is used for function and variable naming.
10. UpperCamelCase is used for general naming. 10. UpperCamelCase is used for general naming.
11. Interface names should not be marked with an uppercase `I`. 11. Interface names should not be marked with an uppercase `I`.
@ -142,6 +131,7 @@ Unless otherwise specified, the following applies to all code:
```typescript ```typescript
let errorMessage: Optional<string>; let errorMessage: Optional<string>;
``` ```
14. Objects, arrays, enums and so on must have each line terminated with a comma: 14. Objects, arrays, enums and so on must have each line terminated with a comma:
```typescript ```typescript
@ -150,21 +140,16 @@ Unless otherwise specified, the following applies to all code:
else: 2, else: 2,
}; };
const arr = [ const arr = ["one", "two"];
"one",
"two",
];
enum Thing { enum Thing {
Foo, Foo,
Bar, Bar,
} }
doThing( doThing("arg1", "arg2");
"arg1",
"arg2",
);
``` ```
15. Objects can use shorthand declarations, including mixing of types. 15. Objects can use shorthand declarations, including mixing of types.
```typescript ```typescript
@ -175,6 +160,7 @@ Unless otherwise specified, the following applies to all code:
// ... or ... // ... or ...
{ room, prop: this.prop } { room, prop: this.prop }
``` ```
16. Object keys should always be non-strings when possible. 16. Object keys should always be non-strings when possible.
```typescript ```typescript
@ -184,11 +170,13 @@ Unless otherwise specified, the following applies to all code:
[EventType.RoomMessage]: true, [EventType.RoomMessage]: true,
} }
``` ```
17. Explicitly cast to a boolean. 17. Explicitly cast to a boolean.
```typescript ```typescript
!!stringVar || Boolean(stringVar) !!stringVar || Boolean(stringVar);
``` ```
18. Use `switch` statements when checking against more than a few enum-like values. 18. Use `switch` statements when checking against more than a few enum-like values.
19. Use `const` for constants, `let` for mutability. 19. Use `const` for constants, `let` for mutability.
20. Describe types exhaustively (ensure noImplictAny would pass). 20. Describe types exhaustively (ensure noImplictAny would pass).
@ -200,6 +188,7 @@ Unless otherwise specified, the following applies to all code:
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable. 2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
23. Prefer readonly members over getters backed by a variable, unless an internal setter is required. 23. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
24. Prefer Interfaces for object definitions, and types for parameter-value-only declarations. 24. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
1. Note that an explicit type is optional if not expected to be used outside of the function call, 1. Note that an explicit type is optional if not expected to be used outside of the function call,
unlike in this example: unlike in this example:
@ -214,6 +203,7 @@ Unless otherwise specified, the following applies to all code:
// ... // ...
} }
``` ```
25. Variables/properties which are `public static` should also be `readonly` when possible. 25. Variables/properties which are `public static` should also be `readonly` when possible.
26. Interface and type properties are terminated with semicolons, not commas. 26. Interface and type properties are terminated with semicolons, not commas.
27. Prefer arrow formatting when declaring functions for interfaces/types: 27. Prefer arrow formatting when declaring functions for interfaces/types:
@ -223,6 +213,7 @@ Unless otherwise specified, the following applies to all code:
myCallback: (arg: string) => Promise<void>; myCallback: (arg: string) => Promise<void>;
} }
``` ```
28. Prefer a type definition over an inline type. For example, define an interface. 28. Prefer a type definition over an inline type. For example, define an interface.
29. Always prefer to add types or declare a type over the use of `any`. Prefer inferred types 29. Always prefer to add types or declare a type over the use of `any`. Prefer inferred types
when they are not `any`. when they are not `any`.
@ -231,6 +222,7 @@ Unless otherwise specified, the following applies to all code:
31. Export only what can be reused. 31. Export only what can be reused.
32. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead 32. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
of truly optional parameters. of truly optional parameters.
1. A notable exception is when the likelihood of a bug is minimal, such as when a function 1. A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the takes an argument that is more often not required than required. An example where the
`?` operator is inappropriate is when taking a room ID: typically the caller should `?` operator is inappropriate is when taking a room ID: typically the caller should
@ -245,6 +237,7 @@ Unless otherwise specified, the following applies to all code:
// ... // ...
} }
``` ```
33. There should be approximately one interface, class, or enum per file unless the file is named 33. There should be approximately one interface, class, or enum per file unless the file is named
"types.ts", "global.d.ts", or ends with "-types.ts". "types.ts", "global.d.ts", or ends with "-types.ts".
1. The file name should match the interface, class, or enum name. 1. The file name should match the interface, class, or enum name.
@ -273,6 +266,7 @@ Unless otherwise specified, the following applies to all code:
const example1 = "simple string"; const example1 = "simple string";
const example2 = 'string containing "double quotes"'; const example2 = 'string containing "double quotes"';
``` ```
39. Prefer async-await to promise-chaining 39. Prefer async-await to promise-chaining
```typescript ```typescript
@ -312,6 +306,7 @@ Inheriting all the rules of TypeScript, the following additionally apply:
} }
} }
``` ```
8. Stores must support using an alternative MatrixClient and dispatcher instance. 8. Stores must support using an alternative MatrixClient and dispatcher instance.
9. Utilities which require JSX must be split out from utilities which do not. This is to prevent import 9. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
cycles during runtime where components accidentally include more of the app than they intended. cycles during runtime where components accidentally include more of the app than they intended.
@ -323,37 +318,28 @@ Inheriting all the rules of TypeScript, the following additionally apply:
```typescript ```typescript
function render() { function render() {
return <Component return <Component prop1="test" prop2={this.state.variable} />;
prop1="test"
prop2={this.state.variable}
/>;
// or // or
return ( return <Component prop1="test" prop2={this.state.variable} />;
<Component
prop1="test"
prop2={this.state.variable}
/>
);
// or if children are needed (infer parens usage) // or if children are needed (infer parens usage)
return <Component return (
prop1="test" <Component prop1="test" prop2={this.state.variable}>
prop2={this.state.variable} {_t("Short string here")}
>{ _t("Short string here") }</Component>; </Component>
);
return (
<Component prop1="test" prop2={this.state.variable}>
return <Component {_t("Longer string here")}
prop1="test" </Component>
prop2={this.state.variable} );
>
{ _t("Longer string here") }
</Component>;
} }
``` ```
13. Curly braces within JSX should be padded with a space, however properties on those components should not. 13. Curly braces within JSX should be padded with a space, however properties on those components should not.
See above code example. See above code example.
14. Functions used as properties should either be defined on the class or stored in a variable. They should not 14. Functions used as properties should either be defined on the class or stored in a variable. They should not
@ -371,7 +357,7 @@ Inheriting all the rules of TypeScript, the following additionally apply:
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not. Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not.
1. Class names must be prefixed with "mx_". 1. Class names must be prefixed with "mx\_".
2. Class names should denote the component which defines them, followed by any context: 2. Class names should denote the component which defines them, followed by any context:
1. mx_MyFoo 1. mx_MyFoo
2. mx_MyFoo_avatar 2. mx_MyFoo_avatar
@ -382,11 +368,13 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
```scss ```scss
.mx_MyFoo { .mx_MyFoo {
& .mx_MyFoo_avatar { // instead of &_avatar & .mx_MyFoo_avatar {
// instead of &_avatar
// ... // ...
} }
} }
``` ```
6. Break multiple selectors over multiple lines this way: 6. Break multiple selectors over multiple lines this way:
```scss ```scss
@ -396,6 +384,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
// ... // ...
} }
``` ```
7. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming. 7. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be 8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
documented for what the values mean: documented for what the values mean:
@ -407,6 +396,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
z-index: 10; // above user avatar, but below dialogs z-index: 10; // above user avatar, but below dialogs
} }
``` ```
9. Avoid the use of `!important`. If necessary, add a comment. 9. Avoid the use of `!important`. If necessary, add a comment.
## Tests ## Tests
@ -431,9 +421,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
// Use "it should..." terminology // Use "it should..." terminology
it("should call the correct API", async () => { it("should call the correct API", async () => {
// test-specific variables go here // test-specific variables go here
// function calls/state changes go here // function calls/state changes go here
// expectations go here // expectations go here
}); });
}); });

View File

@ -26,13 +26,11 @@
"uisi_autorageshake_app": "element-auto-uisi", "uisi_autorageshake_app": "element-auto-uisi",
"default_country_code": "GB", "default_country_code": "GB",
"show_labs_settings": false, "show_labs_settings": false,
"features": { }, "features": {},
"default_federate": true, "default_federate": true,
"default_theme": "light", "default_theme": "light",
"room_directory": { "room_directory": {
"servers": [ "servers": ["matrix.org"]
"matrix.org"
]
}, },
"enable_presence_by_hs_url": { "enable_presence_by_hs_url": {
"https://matrix.org": false, "https://matrix.org": false,

View File

@ -1,17 +1,13 @@
{ {
"name": "Element", "name": "Element",
"description": "A glossy Matrix collaboration client for the web.", "description": "A glossy Matrix collaboration client for the web.",
"repository": { "repository": {
"url": "https://github.com/vector-im/element-web", "url": "https://github.com/vector-im/element-web",
"license": "Apache License 2.0" "license": "Apache License 2.0"
}, },
"bugs": { "bugs": {
"list": "https://github.com/vector-im/element-web/issues", "list": "https://github.com/vector-im/element-web/issues",
"report": "https://github.com/vector-im/element-web/issues/new/choose" "report": "https://github.com/vector-im/element-web/issues/new/choose"
}, },
"keywords": [ "keywords": ["chat", "riot", "matrix"]
"chat",
"riot",
"matrix"
]
} }

View File

@ -15,10 +15,10 @@ Current more parallel flow:
digraph G { digraph G {
node [shape=box]; node [shape=box];
subgraph cluster_0 { subgraph cluster_0 {
color=orange; color=orange;
node [style=filled]; node [style=filled];
label = "index.ts"; label = "index.ts";
entrypoint, s0, ready [shape=point]; entrypoint, s0, ready [shape=point];
rageshake, config, i18n, theme, skin, olm [shape=parallelogram]; rageshake, config, i18n, theme, skin, olm [shape=parallelogram];
@ -52,33 +52,38 @@ digraph G {
skin -> ready [color=red]; skin -> ready [color=red];
theme -> ready [color=red]; theme -> ready [color=red];
i18n -> ready [color=red]; i18n -> ready [color=red];
}
subgraph cluster_1 { }
color = green;
node [style=filled]; subgraph cluster_1 {
label = "init.tsx"; color = green;
node [style=filled];
label = "init.tsx";
ready -> loadApp; ready -> loadApp;
loadApp -> matrixchat; loadApp -> matrixchat;
}
}
} }
</code></pre> </code></pre>
</p> </p>
</details> </details>
Key: Key:
+ Parallelogram: async/await task
+ Box: sync task - Parallelogram: async/await task
+ Diamond: conditional branch - Box: sync task
+ Egg: user interaction - Diamond: conditional branch
+ Blue arrow: async task is allowed to settle but allowed to fail - Egg: user interaction
+ Red arrow: async task success is asserted - Blue arrow: async task is allowed to settle but allowed to fail
- Red arrow: async task success is asserted
Notes: Notes:
+ A task begins when all its dependencies (arrows going into it) are fulfilled.
+ The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake). - A task begins when all its dependencies (arrows going into it) are fulfilled.
+ Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful. - The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake).
- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful.
Underlying dependencies: Underlying dependencies:
![image](https://user-images.githubusercontent.com/2403652/73848977-08624500-4821-11ea-9830-bb0317c41086.png) ![image](https://user-images.githubusercontent.com/2403652/73848977-08624500-4821-11ea-9830-bb0317c41086.png)

View File

@ -33,19 +33,19 @@ someone to add something.
When you're looking through the list, here are some things that might make an When you're looking through the list, here are some things that might make an
issue a **GOOD** choice: issue a **GOOD** choice:
* It is a problem or feature you care about. - It is a problem or feature you care about.
* It concerns a type of code you know a little about. - It concerns a type of code you know a little about.
* You think you can understand what's needed. - You think you can understand what's needed.
* It already has approval from Element Web's designers (look for comments from - It already has approval from Element Web's designers (look for comments from
members of the members of the
[Product](https://github.com/orgs/vector-im/teams/product/members) or [Product](https://github.com/orgs/vector-im/teams/product/members) or
[Design](https://github.com/orgs/vector-im/teams/design/members) teams). [Design](https://github.com/orgs/vector-im/teams/design/members) teams).
Here are some things that might make it a **BAD** choice: Here are some things that might make it a **BAD** choice:
* You don't understand it (maybe add a comment asking a clarifying question). - You don't understand it (maybe add a comment asking a clarifying question).
* It sounds difficult, or is part of a larger change you don't know about. - It sounds difficult, or is part of a larger change you don't know about.
* **It is tagged with `X-Needs-Design` or `X-Needs-Product`.** - **It is tagged with `X-Needs-Design` or `X-Needs-Product`.**
**Element Web's Design and Product teams tend to be very busy**, so if you make **Element Web's Design and Product teams tend to be very busy**, so if you make
changes that require approval from one of those teams, you will probably have changes that require approval from one of those teams, you will probably have

View File

@ -17,7 +17,7 @@ for the desktop app the application will need to be exited fully (including via
## Homeserver configuration ## Homeserver configuration
In order for Element to even start you will need to tell it what homeserver to connect to *by default*. Users will be In order for Element to even start you will need to tell it what homeserver to connect to _by default_. Users will be
able to use a different homeserver if they like, though this can be disabled with `"disable_custom_urls": true` in your able to use a different homeserver if they like, though this can be disabled with `"disable_custom_urls": true` in your
config. config.
@ -26,18 +26,18 @@ One of the following options **must** be supplied:
1. `default_server_config`: The preferred method of setting the homeserver connection information. Simply copy/paste 1. `default_server_config`: The preferred method of setting the homeserver connection information. Simply copy/paste
your [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient) your [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient)
into this field. For example: into this field. For example:
```json ```json
{ {
"default_server_config": { "default_server_config": {
"m.homeserver": { "m.homeserver": {
"base_url": "https://matrix-client.matrix.org" "base_url": "https://matrix-client.matrix.org"
}, },
"m.identity_server": { "m.identity_server": {
"base_url": "https://vector.im" "base_url": "https://vector.im"
} }
} }
} }
``` ```
2. `default_server_name`: A different method of connecting to the homeserver by looking up the connection information 2. `default_server_name`: A different method of connecting to the homeserver by looking up the connection information
using `.well-known`. When using this option, simply use your server's domain name (the part at the end of user IDs): using `.well-known`. When using this option, simply use your server's domain name (the part at the end of user IDs):
`"default_server_name": "matrix.org"` `"default_server_name": "matrix.org"`
@ -58,10 +58,10 @@ To force a labs flag on or off, use the following:
```json ```json
{ {
"features": { "features": {
"feature_you_want_to_turn_on": true, "feature_you_want_to_turn_on": true,
"feature_you_want_to_keep_off": false "feature_you_want_to_keep_off": false
} }
} }
``` ```
@ -82,25 +82,25 @@ instance. As of writing those settings are not fully documented, however a few a
inputs. inputs.
3. `room_directory`: Optionally defines how the room directory component behaves. Currently only a single property, `servers` 3. `room_directory`: Optionally defines how the room directory component behaves. Currently only a single property, `servers`
is supported to add additional servers to the dropdown. For example: is supported to add additional servers to the dropdown. For example:
```json ```json
{ {
"room_directory": { "room_directory": {
"servers": ["matrix.org", "example.org"] "servers": ["matrix.org", "example.org"]
} }
} }
``` ```
4. `setting_defaults`: Optional configuration for settings which are not described by this document and support the `config` 4. `setting_defaults`: Optional configuration for settings which are not described by this document and support the `config`
level. This list is incomplete. For example: level. This list is incomplete. For example:
```json ```json
{ {
"setting_defaults": { "setting_defaults": {
"MessageComposerInput.showStickersButton": false, "MessageComposerInput.showStickersButton": false,
"MessageComposerInput.showPollsButton": false "MessageComposerInput.showPollsButton": false
} }
} }
``` ```
These values will take priority over the hardcoded defaults for the settings. For a list of available settings, see These values will take priority over the hardcoded defaults for the settings. For a list of available settings, see
[Settings.tsx](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.tsx). [Settings.tsx](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.tsx).
## Customisation & branding ## Customisation & branding
@ -170,16 +170,16 @@ Together, these two options might look like the following in your config:
```json ```json
{ {
"desktop_builds": { "desktop_builds": {
"available": true, "available": true,
"logo": "https://example.org/assets/logo-small.svg", "logo": "https://example.org/assets/logo-small.svg",
"url": "https://example.org/not_element/download" "url": "https://example.org/not_element/download"
}, },
"mobile_builds": { "mobile_builds": {
"ios": null, "ios": null,
"android": "https://example.org/not_element/android", "android": "https://example.org/not_element/android",
"fdroid": "https://example.org/not_element/fdroid" "fdroid": "https://example.org/not_element/fdroid"
} }
} }
``` ```
@ -210,18 +210,18 @@ Together, the options might look like this in your config:
```json ```json
{ {
"branding": { "branding": {
"welcome_background_url": "https://example.org/assets/background.jpg", "welcome_background_url": "https://example.org/assets/background.jpg",
"auth_header_logo_url": "https://example.org/assets/logo.svg", "auth_header_logo_url": "https://example.org/assets/logo.svg",
"auth_footer_links": [ "auth_footer_links": [
{"text": "FAQ", "url": "https://example.org/faq"}, { "text": "FAQ", "url": "https://example.org/faq" },
{"text": "Donate", "url": "https://example.org/donate"} { "text": "Donate", "url": "https://example.org/donate" }
] ]
}, },
"embedded_pages": { "embedded_pages": {
"welcome_url": "https://example.org/assets/welcome.html", "welcome_url": "https://example.org/assets/welcome.html",
"home_url": "https://example.org/assets/home.html" "home_url": "https://example.org/assets/home.html"
} }
} }
``` ```
@ -240,15 +240,15 @@ When Element is deployed alongside a homeserver with SSO-only login, some option
2. `sso_redirect_options`: Options to define how to handle unauthenticated users. If the object contains `"immediate": true`, then 2. `sso_redirect_options`: Options to define how to handle unauthenticated users. If the object contains `"immediate": true`, then
all unauthenticated users will be automatically redirected to the SSO system to start their login. If instead you'd only like to all unauthenticated users will be automatically redirected to the SSO system to start their login. If instead you'd only like to
have users which land on the welcome page to be redirected, use `"on_welcome_page": true`. As an example: have users which land on the welcome page to be redirected, use `"on_welcome_page": true`. As an example:
```json ```json
{ {
"sso_redirect_options": { "sso_redirect_options": {
"immediate": false, "immediate": false,
"on_welcome_page": true "on_welcome_page": true
} }
} }
``` ```
It is most common to use the `immediate` flag instead of `on_welcome_page`. It is most common to use the `immediate` flag instead of `on_welcome_page`.
## VoIP / Jitsi calls ## VoIP / Jitsi calls
@ -261,77 +261,77 @@ More information about the Jitsi setup can be found [here](./jitsi.md).
The VoIP and Jitsi options are: The VoIP and Jitsi options are:
1. `jitsi`: Optional configuration for how to start Jitsi conferences. Currently can only contain a single `preferred_domain` 1. `jitsi`: Optional configuration for how to start Jitsi conferences. Currently can only contain a single `preferred_domain`
value which points at the domain of the Jitsi instance. Defaults to `meet.element.io`. This is *not* used if the Jitsi widget value which points at the domain of the Jitsi instance. Defaults to `meet.element.io`. This is _not_ used if the Jitsi widget
was created by an integration manager, or if the homeserver provides Jitsi information in `/.well-known/matrix/client`. For was created by an integration manager, or if the homeserver provides Jitsi information in `/.well-known/matrix/client`. For
example: example:
```json ```json
{ {
"jitsi": { "jitsi": {
"preferred_domain": "meet.jit.si" "preferred_domain": "meet.jit.si"
} }
} }
``` ```
2. `jitsi_widget`: Optional configuration for the built-in Jitsi widget. Currently can only contain a single `skip_built_in_welcome_screen` 2. `jitsi_widget`: Optional configuration for the built-in Jitsi widget. Currently can only contain a single `skip_built_in_welcome_screen`
value, denoting whether the "Join Conference" button should be shown. When `true` (default `false`), Jitsi calls will skip to value, denoting whether the "Join Conference" button should be shown. When `true` (default `false`), Jitsi calls will skip to
the call instead of having a screen with a single button on it. This is most useful if the Jitsi instance being used already the call instead of having a screen with a single button on it. This is most useful if the Jitsi instance being used already
has a landing page for users to test audio and video before joining the call, otherwise users will automatically join the call. has a landing page for users to test audio and video before joining the call, otherwise users will automatically join the call.
For example: For example:
```json ```json
{ {
"jitsi_widget": { "jitsi_widget": {
"skip_built_in_welcome_screen": true "skip_built_in_welcome_screen": true
} }
} }
``` ```
3. `voip`: Optional configuration for various VoIP features. Currently can only contain a single `obey_asserted_identity` value to 3. `voip`: Optional configuration for various VoIP features. Currently can only contain a single `obey_asserted_identity` value to
send MSC3086-style asserted identity messages during VoIP calls in the room corresponding to the asserted identity. This *must* send MSC3086-style asserted identity messages during VoIP calls in the room corresponding to the asserted identity. This _must_
only be set in trusted environments. The option defaults to `false`. For example: only be set in trusted environments. The option defaults to `false`. For example:
```json ```json
{ {
"voip": { "voip": {
"obey_asserted_identity": false "obey_asserted_identity": false
} }
} }
``` ```
4. `widget_build_url`: Optional URL to have Element make a request to when a user presses the voice/video call buttons in the app, 4. `widget_build_url`: Optional URL to have Element make a request to when a user presses the voice/video call buttons in the app,
if a call would normally be started by the action. The URL will be called with a `roomId` query parameter to identify the room if a call would normally be started by the action. The URL will be called with a `roomId` query parameter to identify the room
being called in. The URL must respond with a JSON object similar to the following: being called in. The URL must respond with a JSON object similar to the following:
```json ```json
{ {
"widget_id": "$arbitrary_string", "widget_id": "$arbitrary_string",
"widget": { "widget": {
"creatorUserId": "@user:example.org", "creatorUserId": "@user:example.org",
"id": "$the_same_widget_id", "id": "$the_same_widget_id",
"type": "m.custom", "type": "m.custom",
"waitForIframeLoad": true, "waitForIframeLoad": true,
"name": "My Widget Name Here", "name": "My Widget Name Here",
"avatar_url": "mxc://example.org/abc123", "avatar_url": "mxc://example.org/abc123",
"url": "https://example.org/widget.html", "url": "https://example.org/widget.html",
"data": { "data": {
"title": "Subtitle goes here" "title": "Subtitle goes here"
} }
}, },
"layout": { "layout": {
"container": "top", "container": "top",
"index": 0, "index": 0,
"width": 65, "width": 65,
"height": 50 "height": 50
} }
} }
``` ```
The `widget` is the `content` of a normal widget state event. The `layout` is the layout specifier for the widget being created, The `widget` is the `content` of a normal widget state event. The `layout` is the layout specifier for the widget being created,
as defined by the `io.element.widgets.layout` state event. as defined by the `io.element.widgets.layout` state event.
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed 5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
at any time without notice. at any time without notice.
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys: 6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
- `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental - `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental
and may be removed at any time without notice. Defaults to `https://call.element.io`. and may be removed at any time without notice. Defaults to `https://call.element.io`.
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in - `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`. the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
- `participant_limit`: The maximum number of users who can join a call; if - `participant_limit`: The maximum number of users who can join a call; if
this number is exceeded, the user will not be able to join a given call. this number is exceeded, the user will not be able to join a given call.
- `brand`: Optional name for the app. Defaults to `Element Call`. This is - `brand`: Optional name for the app. Defaults to `Element Call`. This is
used throughout the application in various strings/locations. used throughout the application in various strings/locations.
## Bug reporting ## Bug reporting
@ -344,7 +344,7 @@ If you run your own rageshake server to collect bug reports, the following optio
alongside the rageshake so the rageshake server can filter them by app name. By default, this will be `element-web`, as with any other alongside the rageshake so the rageshake server can filter them by app name. By default, this will be `element-web`, as with any other
rageshake submitted by the app. rageshake submitted by the app.
If you are using the element.io rageshake server, please set this to `element-auto-uisi` so we can better filter them. If you are using the element.io rageshake server, please set this to `element-auto-uisi` so we can better filter them.
If you would like to use [Sentry](https://sentry.io/) for rageshake data, add a `sentry` object to your config with the following values: If you would like to use [Sentry](https://sentry.io/) for rageshake data, add a `sentry` object to your config with the following values:
@ -355,10 +355,10 @@ For example:
```json ```json
{ {
"sentry": { "sentry": {
"dsn": "dsn-goes-here", "dsn": "dsn-goes-here",
"environment": "production" "environment": "production"
} }
} }
``` ```
@ -375,15 +375,15 @@ If you would like to use Scalar, the integration manager maintained by Element,
```json ```json
{ {
"integrations_ui_url": "https://scalar.vector.im/", "integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api", "integrations_rest_url": "https://scalar.vector.im/api",
"integrations_widgets_urls": [ "integrations_widgets_urls": [
"https://scalar.vector.im/_matrix/integrations/v1", "https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api", "https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1", "https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api", "https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api" "https://scalar-staging.riot.im/scalar/api"
] ]
} }
``` ```
@ -393,9 +393,9 @@ If you would like to include a custom message when someone is reporting an event
```json ```json
{ {
"report_event": { "report_event": {
"admin_message_md": "Please be sure to review our [terms of service](https://example.org/terms) before reporting a message." "admin_message_md": "Please be sure to review our [terms of service](https://example.org/terms) before reporting a message."
} }
} }
``` ```
@ -403,9 +403,7 @@ To add additional "terms and conditions" links throughout the app, use the follo
```json ```json
{ {
"terms_and_conditions_links": [ "terms_and_conditions_links": [{ "text": "Code of conduct", "url": "https://example.org/code-of-conduct" }]
{ "text": "Code of conduct", "url": "https://example.org/code-of-conduct" }
]
} }
``` ```
@ -422,7 +420,7 @@ analytics are deemed impossible and the user won't be asked to opt in to the sys
There are additional root-level options which can be specified: There are additional root-level options which can be specified:
1. `analytics_owner`: the company name used in dialogs talking about analytics - this defaults to `brand`, 1. `analytics_owner`: the company name used in dialogs talking about analytics - this defaults to `brand`,
and is useful when the provider of analytics is different from the provider of the Element instance. and is useful when the provider of analytics is different from the provider of the Element instance.
2. `privacy_policy_url`: URL to the privacy policy including the analytics collection policy. 2. `privacy_policy_url`: URL to the privacy policy including the analytics collection policy.
## Server hosting links ## Server hosting links
@ -435,26 +433,26 @@ will not be shown to the user.
of `utm_campaign` to denote which link the user clicked on within the app. Only ever applies to matrix.org users specifically. of `utm_campaign` to denote which link the user clicked on within the app. Only ever applies to matrix.org users specifically.
2. `host_signup`: Optional configuration for an account importer to your hosting platform. The API surface of this is not documented 2. `host_signup`: Optional configuration for an account importer to your hosting platform. The API surface of this is not documented
at the moment, but can be configured with the following subproperties: at the moment, but can be configured with the following subproperties:
1. `brand`: The brand name to use. 1. `brand`: The brand name to use.
2. `url`: The iframe URL for the importer. 2. `url`: The iframe URL for the importer.
3. `domains`: The homeserver domains to show the importer to. 3. `domains`: The homeserver domains to show the importer to.
4. `cookie_policy_url`: The URL to the cookie policy for the importer. 4. `cookie_policy_url`: The URL to the cookie policy for the importer.
5. `privacy_policy_url`: The URL to the privacy policy for the importer. 5. `privacy_policy_url`: The URL to the privacy policy for the importer.
6. `terms_of_service_url`: The URL to the terms of service for the importer. 6. `terms_of_service_url`: The URL to the terms of service for the importer.
If you're looking to mirror a setup from our production/development environments, the following config should be used: If you're looking to mirror a setup from our production/development environments, the following config should be used:
```json ```json
{ {
"hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web", "hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web",
"host_signup": { "host_signup": {
"brand": "Element Home", "brand": "Element Home",
"domains": [ "matrix.org" ], "domains": ["matrix.org"],
"url": "https://ems.element.io/element-home/in-app-loader", "url": "https://ems.element.io/element-home/in-app-loader",
"cookie_policy_url": "https://element.io/cookie-policy", "cookie_policy_url": "https://element.io/cookie-policy",
"privacy_policy_url": "https://element.io/privacy", "privacy_policy_url": "https://element.io/privacy",
"terms_of_service_url": "https://element.io/terms-of-service" "terms_of_service_url": "https://element.io/terms-of-service"
} }
} }
``` ```
@ -467,10 +465,10 @@ set this value to the following at a minimum:
```json ```json
{ {
"enable_presence_by_hs_url": { "enable_presence_by_hs_url": {
"https://matrix.org": false, "https://matrix.org": false,
"https://matrix-client.matrix.org": false "https://matrix-client.matrix.org": false
} }
} }
``` ```
@ -487,8 +485,8 @@ Element will check multiple sources when looking for an identity server to use i
the following order of preference: the following order of preference:
1. The identity server set in the user's account data 1. The identity server set in the user's account data
* For a new user, no value is present in their account data. It is only set - For a new user, no value is present in their account data. It is only set
if the user visits Settings and manually changes their identity server. if the user visits Settings and manually changes their identity server.
2. The identity server provided by the `.well-known` lookup that occurred at 2. The identity server provided by the `.well-known` lookup that occurred at
login login
3. The identity server provided by the Riot config file 3. The identity server provided by the Riot config file
@ -514,40 +512,40 @@ preferences.
Currently, the following UI feature flags are supported: Currently, the following UI feature flags are supported:
* `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application. - `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application.
* `UIFeature.feedback` - Whether prompts to supply feedback are shown. - `UIFeature.feedback` - Whether prompts to supply feedback are shown.
* `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled, - `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled,
Jitsi widgets will still work though they cannot easily be added. Jitsi widgets will still work though they cannot easily be added.
* `UIFeature.widgets` - Whether or not widgets will be shown. - `UIFeature.widgets` - Whether or not widgets will be shown.
* `UIFeature.flair` - Whether or not community flair is shown in rooms. - `UIFeature.flair` - Whether or not community flair is shown in rooms.
* `UIFeature.communities` - Whether or not to show any UI related to communities. Implicitly - `UIFeature.communities` - Whether or not to show any UI related to communities. Implicitly
disables `UIFeature.flair` when disabled. disables `UIFeature.flair` when disabled.
* `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and - `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and
user settings are shown to the user. user settings are shown to the user.
* `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog - `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog
is shown. is shown.
* `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog - `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog
are shown. are shown.
* `UIFeature.identityServer` - Whether or not functionality requiring an identity server - `UIFeature.identityServer` - Whether or not functionality requiring an identity server
is shown. When disabled, the user will not be able to interact with the identity is shown. When disabled, the user will not be able to interact with the identity
server (sharing email addresses, 3PID invites, etc). server (sharing email addresses, 3PID invites, etc).
* `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs) - `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs)
is shown. Typically this is considered "contact information" on the homeserver, and is is shown. Typically this is considered "contact information" on the homeserver, and is
not directly related to the identity server. not directly related to the identity server.
* `UIFeature.registration` - Whether or not the registration page is accessible. Typically - `UIFeature.registration` - Whether or not the registration page is accessible. Typically
useful if accounts are managed externally. useful if accounts are managed externally.
* `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically - `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically
useful if accounts are managed externally. useful if accounts are managed externally.
* `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically - `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically
useful if accounts are managed externally. useful if accounts are managed externally.
* `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the - `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the
user. user.
* `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user. - `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user.
This should only be used if the room history visibility options are managed by the server. This should only be used if the room history visibility options are managed by the server.
* `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the - `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the
timeline for recent messages. When false day dates will be used. timeline for recent messages. When false day dates will be used.
* `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults - `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
to true. to true.
## Undocumented / developer options ## Undocumented / developer options

View File

@ -26,7 +26,6 @@ The home page can be overridden in `config.json` to provide all users of an elem
} }
``` ```
## `home.html` Example ## `home.html` Example
The following is a simple example for a custom `home.html`: The following is a simple example for a custom `home.html`:
@ -62,4 +61,3 @@ It may be needed to set CORS headers for the `home.html` to enable element-deskt
``` ```
add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Origin *;
``` ```

View File

@ -30,7 +30,7 @@ maintenance.
**Note**: The project deliberately does not exclude `customisations.json` from Git. **Note**: The project deliberately does not exclude `customisations.json` from Git.
This is to ensure that in shared projects it's possible to have a common config. By This is to ensure that in shared projects it's possible to have a common config. By
default, Element Web does *not* ship with this file to prevent conflicts. default, Element Web does _not_ ship with this file to prevent conflicts.
### Custom components ### Custom components
@ -41,9 +41,10 @@ that properties/state machines won't change.
### Component visibility customisation ### Component visibility customisation
UI for some actions can be hidden via the ComponentVisibility customisation: UI for some actions can be hidden via the ComponentVisibility customisation:
- inviting users to rooms and spaces,
- creating rooms, - inviting users to rooms and spaces,
- creating spaces, - creating rooms,
- creating spaces,
To customise visibility create a customisation module from [ComponentVisibility](https://github.com/matrix-org/matrix-react-sdk/blob/master/src/customisations/ComponentVisibility.ts) following the instructions above. To customise visibility create a customisation module from [ComponentVisibility](https://github.com/matrix-org/matrix-react-sdk/blob/master/src/customisations/ComponentVisibility.ts) following the instructions above.
@ -55,6 +56,7 @@ might be shown to the user, but they won't have permission to invite users to
the current room: the button will appear disabled. the current room: the button will appear disabled.
For example, to only allow users who meet a certain condition to create spaces: For example, to only allow users who meet a certain condition to create spaces:
```typescript ```typescript
function shouldShowComponent(component: UIComponent): boolean { function shouldShowComponent(component: UIComponent): boolean {
if (component === UIComponent.CreateSpaces) { if (component === UIComponent.CreateSpaces) {
@ -65,4 +67,5 @@ function shouldShowComponent(component: UIComponent): boolean {
return true; return true;
} }
``` ```
In this example, all UI related to creating a space will be hidden unless the users meets the custom condition. In this example, all UI related to creating a space will be hidden unless the users meets the custom condition.

View File

@ -10,9 +10,9 @@ Set the following on your homeserver's
```json ```json
{ {
"io.element.e2ee": { "io.element.e2ee": {
"default": false "default": false
} }
} }
``` ```
@ -29,9 +29,9 @@ following on your homeserver's `/.well-known/matrix/client` config:
```json ```json
{ {
"io.element.e2ee": { "io.element.e2ee": {
"secure_backup_required": true "secure_backup_required": true
} }
} }
``` ```
@ -44,9 +44,9 @@ only offer one of these, you can signal this via the
```json ```json
{ {
"io.element.e2ee": { "io.element.e2ee": {
"secure_backup_setup_methods": ["passphrase"] "secure_backup_setup_methods": ["passphrase"]
} }
} }
``` ```

View File

@ -5,10 +5,10 @@ flexibility and control over when and where those features are enabled.
For example, flags make the following things possible: For example, flags make the following things possible:
* Extended testing of a feature via labs on develop - Extended testing of a feature via labs on develop
* Enabling features when ready instead of the first moment the code is released - Enabling features when ready instead of the first moment the code is released
* Testing a feature with a specific set of users (by enabling only on a specific - Testing a feature with a specific set of users (by enabling only on a specific
Element instance) Element instance)
The size of the feature controlled by a feature flag may vary widely: it could The size of the feature controlled by a feature flag may vary widely: it could
be a large project like reactions or a smaller change to an existing algorithm. be a large project like reactions or a smaller change to an existing algorithm.
@ -37,6 +37,7 @@ When starting work on a feature, we should create a matching feature flag:
1. Add a new 1. Add a new
[setting](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.tsx) [setting](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.tsx)
of the form: of the form:
```js ```js
"feature_cats": { "feature_cats": {
isFeature: true, isFeature: true,
@ -45,10 +46,13 @@ When starting work on a feature, we should create a matching feature flag:
default: false, default: false,
}, },
``` ```
2. Check whether the feature is enabled as appropriate: 2. Check whether the feature is enabled as appropriate:
```js ```js
SettingsStore.getValue("feature_cats") SettingsStore.getValue("feature_cats");
``` ```
3. Document the feature in the [labs documentation](https://github.com/vector-im/element-web/blob/develop/docs/labs.md) 3. Document the feature in the [labs documentation](https://github.com/vector-im/element-web/blob/develop/docs/labs.md)
With these steps completed, the feature is disabled by default, but can be With these steps completed, the feature is disabled by default, but can be
@ -88,12 +92,14 @@ cover these cases, change the setting's `default` in `Settings.tsx` to `true`.
Once we're confident that a feature is working well, we should remove or convert the flag. Once we're confident that a feature is working well, we should remove or convert the flag.
If the feature is meant to be turned off/on by the user: If the feature is meant to be turned off/on by the user:
1. Remove `isFeature` from the [setting](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.ts) 1. Remove `isFeature` from the [setting](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.ts)
2. Change the `default` to `true` (if desired). 2. Change the `default` to `true` (if desired).
3. Remove the feature from the [labs documentation](https://github.com/vector-im/element-web/blob/develop/docs/labs.md) 3. Remove the feature from the [labs documentation](https://github.com/vector-im/element-web/blob/develop/docs/labs.md)
4. Celebrate! 🥳 4. Celebrate! 🥳
If the feature is meant to be forced on (non-configurable): If the feature is meant to be forced on (non-configurable):
1. Remove the [setting](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.ts) 1. Remove the [setting](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/settings/Settings.ts)
2. Remove all `getValue` lines that test for the feature. 2. Remove all `getValue` lines that test for the feature.
3. Remove the feature from the [labs documentation](https://github.com/vector-im/element-web/blob/develop/docs/labs.md) 3. Remove the feature from the [labs documentation](https://github.com/vector-im/element-web/blob/develop/docs/labs.md)

View File

@ -1,7 +1,7 @@
# Jitsi wrapper developer docs # Jitsi wrapper developer docs
*If you're looking for information on how to set up Jitsi in your Element, see _If you're looking for information on how to set up Jitsi in your Element, see
[jitsi.md](./jitsi.md) instead.* [jitsi.md](./jitsi.md) instead._
These docs are for developers wondering how the different conference buttons work These docs are for developers wondering how the different conference buttons work
within Element. If you're not a developer, you're probably looking for [jitsi.md](./jitsi.md). within Element. If you're not a developer, you're probably looking for [jitsi.md](./jitsi.md).
@ -46,24 +46,24 @@ end up creating a widget with a URL like `https://integrations.example.org?widge
The integration manager's wrapper will typically have another iframe to isolate the The integration manager's wrapper will typically have another iframe to isolate the
widget from the client by yet another layer. The wrapper often provides other functionality widget from the client by yet another layer. The wrapper often provides other functionality
which might not be available on the embedded site, such as a fullscreen button or the which might not be available on the embedded site, such as a fullscreen button or the
communication layer with the client (all widgets *should* be talking to the client communication layer with the client (all widgets _should_ be talking to the client
over `postMessage`, even if they aren't going to be using the widget APIs). over `postMessage`, even if they aren't going to be using the widget APIs).
Widgets added with the `/addwidget` command will *not* be wrapped as they are not going Widgets added with the `/addwidget` command will _not_ be wrapped as they are not going
through an integration manager. The widgets themselves *should* also work outside of through an integration manager. The widgets themselves _should_ also work outside of
Element. Widgets currently have a "pop out" button which opens them in a new tab and Element. Widgets currently have a "pop out" button which opens them in a new tab and
therefore have no connection back to Riot. therefore have no connection back to Riot.
## Jitsi widgets from integration managers ## Jitsi widgets from integration managers
Integration managers will create an entire widget event and send it over `postMessage` Integration managers will create an entire widget event and send it over `postMessage`
for the client to add to the room. This means that the integration manager gets to for the client to add to the room. This means that the integration manager gets to
decide the conference domain, conference name, and other aspects of the widget. As decide the conference domain, conference name, and other aspects of the widget. As
a result, users can end up with a Jitsi widget that does not use the same conference a result, users can end up with a Jitsi widget that does not use the same conference
server they specified in their config.json - this is expected. server they specified in their config.json - this is expected.
Some integration managers allow the user to change the conference name while others Some integration managers allow the user to change the conference name while others
will generate one for the user. will generate one for the user.
## Jitsi widgets generated by Element itself ## Jitsi widgets generated by Element itself
@ -79,7 +79,7 @@ The Jitsi widget created by Element uses a local `jitsi.html` wrapper (or one ho
required `postMessage` calls are fulfilled. required `postMessage` calls are fulfilled.
**Note**: Per [jitsi.md](./jitsi.md) the `preferredDomain` can also come from the server's **Note**: Per [jitsi.md](./jitsi.md) the `preferredDomain` can also come from the server's
client .well-known data. client .well-known data.
## The Jitsi wrapper in Element ## The Jitsi wrapper in Element
@ -92,9 +92,9 @@ and less risky to load. The local wrapper URL is populated with the conference i
from the original widget (which could be a v1 or v2 widget) so the user joins the right from the original widget (which could be a v1 or v2 widget) so the user joins the right
call. call.
Critically, when the widget URL is reconstructed it does *not* take into account the Critically, when the widget URL is reconstructed it does _not_ take into account the
config.json's `preferredDomain` for Jitsi. If it did this, users would end up on different config.json's `preferredDomain` for Jitsi. If it did this, users would end up on different
conference servers and therefore different calls entirely. conference servers and therefore different calls entirely.
**Note**: Per [jitsi.md](./jitsi.md) the `preferredDomain` can also come from the server's **Note**: Per [jitsi.md](./jitsi.md) the `preferredDomain` can also come from the server's
client .well-known data. client .well-known data.

View File

@ -24,13 +24,14 @@ Element will use the Jitsi server that is embedded in the widget, even if it is
one you configured. This is because conference calls must be held on a single Jitsi one you configured. This is because conference calls must be held on a single Jitsi
server and cannot be split over multiple servers. server and cannot be split over multiple servers.
However, you can configure Element to *start* a conference with your Jitsi server by adding However, you can configure Element to _start_ a conference with your Jitsi server by adding
to your [config](./config.md) the following: to your [config](./config.md) the following:
```json ```json
{ {
"jitsi": { "jitsi": {
"preferredDomain": "your.jitsi.example.org" "preferredDomain": "your.jitsi.example.org"
} }
} }
``` ```
@ -46,11 +47,12 @@ domain will appear later in the URL as a configuration parameter.
**Hint**: If you want everyone on your homeserver to use the same Jitsi server by **Hint**: If you want everyone on your homeserver to use the same Jitsi server by
default, and you are using element-web 1.6 or newer, set the following on your homeserver's default, and you are using element-web 1.6 or newer, set the following on your homeserver's
`/.well-known/matrix/client` config: `/.well-known/matrix/client` config:
```json ```json
{ {
"im.vector.riot.jitsi": { "im.vector.riot.jitsi": {
"preferredDomain": "your.jitsi.example.org" "preferredDomain": "your.jitsi.example.org"
} }
} }
``` ```

View File

@ -1,5 +1,4 @@
Running in Kubernetes # Running in Kubernetes
=====================
In case you would like to deploy element-web in a kubernetes cluster you can use In case you would like to deploy element-web in a kubernetes cluster you can use
the provided Kubernetes example below as a starting point. Note that this example assumes the the provided Kubernetes example below as a starting point. Note that this example assumes the
@ -178,4 +177,3 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my-
number: 80 number: 80
--- ---

View File

@ -122,7 +122,7 @@ Switches to a new room search experience.
## Extensible events rendering (`feature_extensible_events`) [In Development] ## Extensible events rendering (`feature_extensible_events`) [In Development]
*Intended for developer use only at the moment.* _Intended for developer use only at the moment._
Extensible Events are a [new event format](https://github.com/matrix-org/matrix-doc/pull/1767) which Extensible Events are a [new event format](https://github.com/matrix-org/matrix-doc/pull/1767) which
supports graceful fallback in unknown event types. Instead of rendering nothing or a blank space, events supports graceful fallback in unknown event types. Instead of rendering nothing or a blank space, events
@ -159,7 +159,7 @@ Threading allows users to branch out a new conversation from the main timeline o
Threads can be access by clicking their summary below the root event on the room timeline. Users can find a comprehensive list of threads by click the icon on the room header button. Threads can be access by clicking their summary below the root event on the room timeline. Users can find a comprehensive list of threads by click the icon on the room header button.
This feature might work in degraded mode if the homeserver a user is connected to does not advertise support for the unstable feature `org.matrix.msc3440` when calling the `/versions` API endpoint. This feature might work in degraded mode if the homeserver a user is connected to does not advertise support for the unstable feature `org.matrix.msc3440` when calling the `/versions` API endpoint.
## Video rooms (`feature_video_rooms`) ## Video rooms (`feature_video_rooms`)

View File

@ -1,11 +1,11 @@
## Memory leaks ## Memory leaks
Element usually emits slow behaviour just before it is about to crash. Getting a Element usually emits slow behaviour just before it is about to crash. Getting a
memory snapshot (below) just before that happens is ideal in figuring out what memory snapshot (below) just before that happens is ideal in figuring out what
is going wrong. is going wrong.
Common symptoms are clicking on a room and it feels like the tab froze and scrolling Common symptoms are clicking on a room and it feels like the tab froze and scrolling
becoming jumpy/staggered. becoming jumpy/staggered.
If you receive a white screen (electron) or the chrome crash page, it is likely If you receive a white screen (electron) or the chrome crash page, it is likely
run out of memory and it is too late for a memory profile. Please do report when run out of memory and it is too late for a memory profile. Please do report when
@ -22,8 +22,8 @@ and anything newer is still in the warmup stages of the app.
**Memory profiles can contain sensitive information.** If you are submitting a memory **Memory profiles can contain sensitive information.** If you are submitting a memory
profile to us for debugging purposes, please pick the appropriate Element developer and profile to us for debugging purposes, please pick the appropriate Element developer and
send them over an encrypted private message. *Do not share your memory profile in send them over an encrypted private message. _Do not share your memory profile in
public channels or with people you do not trust.* public channels or with people you do not trust._
### Taking a memory profile (Firefox) ### Taking a memory profile (Firefox)

View File

@ -34,6 +34,7 @@ our [ILAG module](https://github.com/vector-im/element-web-ilag-module) which wi
structure of a module is and how it works. structure of a module is and how it works.
The following requirements are key for any module: The following requirements are key for any module:
1. The module must depend on `@matrix-org/react-sdk-module-api` (usually as a dev dependency). 1. The module must depend on `@matrix-org/react-sdk-module-api` (usually as a dev dependency).
2. The module's `main` entrypoint must have a `default` export for the `RuntimeModule` instance, supporting a constructor 2. The module's `main` entrypoint must have a `default` export for the `RuntimeModule` instance, supporting a constructor
which takes a single parameter: a `ModuleApi` instance. This instance is passed to `super()`. which takes a single parameter: a `ModuleApi` instance. This instance is passed to `super()`.

View File

@ -10,53 +10,53 @@ When reviewing code, here are some things we look for and also things we avoid:
### We review for ### We review for
* Correctness - Correctness
* Performance - Performance
* Accessibility - Accessibility
* Security - Security
* Quality via automated and manual testing - Quality via automated and manual testing
* Comments and documentation where needed - Comments and documentation where needed
* Sharing knowledge of different areas among the team - Sharing knowledge of different areas among the team
* Ensuring it's something we're comfortable maintaining for the long term - Ensuring it's something we're comfortable maintaining for the long term
* Progress indicators and local echo where appropriate with network activity - Progress indicators and local echo where appropriate with network activity
### We should avoid ### We should avoid
* Style nits that are already handled by the linter - Style nits that are already handled by the linter
* Dramatically increasing scope - Dramatically increasing scope
### Good practices ### Good practices
* Use empathetic language - Use empathetic language
* See also [Mindful Communication in Code - See also [Mindful Communication in Code
Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e) Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e)
and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/) and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/)
* Authors should prefer smaller commits for easier reviewing and bisection - Authors should prefer smaller commits for easier reviewing and bisection
* Reviewers should be explicit about required versus optional changes - Reviewers should be explicit about required versus optional changes
* Reviews are conversations and the PR author should feel comfortable - Reviews are conversations and the PR author should feel comfortable
discussing and pushing back on changes before making them discussing and pushing back on changes before making them
* Reviewers are encouraged to ask for tests where they believe it is reasonable - Reviewers are encouraged to ask for tests where they believe it is reasonable
* Core team should lead by example through their tone and language - Core team should lead by example through their tone and language
* Take the time to thank and point out good code changes - Take the time to thank and point out good code changes
* Using softer language like "please" and "what do you think?" goes a long way - Using softer language like "please" and "what do you think?" goes a long way
towards making others feel like colleagues working towards a common goal towards making others feel like colleagues working towards a common goal
### Workflow ### Workflow
* Authors should request review from the element-web team by default (if someone on - Authors should request review from the element-web team by default (if someone on
the team is clearly the expert in an area, a direct review request to them may the team is clearly the expert in an area, a direct review request to them may
be more appropriate) be more appropriate)
* Reviewers should remove the team review request and request review from - Reviewers should remove the team review request and request review from
themselves when starting a review to avoid double review themselves when starting a review to avoid double review
* If there are multiple related PRs authors should reference each of the PRs in - If there are multiple related PRs authors should reference each of the PRs in
the others before requesting review. Reviewers might start reviewing from the others before requesting review. Reviewers might start reviewing from
different places and could miss other required PRs. different places and could miss other required PRs.
* Avoid force pushing to a PR after the first round of review - Avoid force pushing to a PR after the first round of review
* Use the GitHub default of merge commits when landing (avoid alternate options - Use the GitHub default of merge commits when landing (avoid alternate options
like squash or rebase) like squash or rebase)
* PR author merges after review (assuming they have write access) - PR author merges after review (assuming they have write access)
* Assign issues only when in progress to indicate to others what can be picked - Assign issues only when in progress to indicate to others what can be picked
up up
## Code Quality ## Code Quality
@ -64,10 +64,10 @@ In the past, we have occasionally written different kinds of tests for
Element and the SDKs, but it hasn't been a consistent focus. Going forward, we'd Element and the SDKs, but it hasn't been a consistent focus. Going forward, we'd
like to change that. like to change that.
* For new features, code reviewers will expect some form of automated testing to - For new features, code reviewers will expect some form of automated testing to
be included by default be included by default
* For bug fixes, regression tests are of course great to have, but we don't want - For bug fixes, regression tests are of course great to have, but we don't want
to block fixes on this, so we won't require them at this time to block fixes on this, so we won't require them at this time
The above policy is not a strict rule, but instead it's meant to be a The above policy is not a strict rule, but instead it's meant to be a
conversation between the author and reviewer. As an author, try to think about conversation between the author and reviewer. As an author, try to think about
@ -104,10 +104,10 @@ perspective.
In more detail, our usual process for changes that affect the UI or alter user In more detail, our usual process for changes that affect the UI or alter user
functionality is: functionality is:
* For changes that will go live when merged, always flag Design and Product - For changes that will go live when merged, always flag Design and Product
teams as appropriate teams as appropriate
* For changes guarded by a feature flag, Design and Product review is not - For changes guarded by a feature flag, Design and Product review is not
required (though may still be useful) since we can continue tweaking required (though may still be useful) since we can continue tweaking
As it can be difficult to review design work from looking at just the changed As it can be difficult to review design work from looking at just the changed
files in a PR, a [preview site](./pr-previews.md) that includes your changes files in a PR, a [preview site](./pr-previews.md) that includes your changes

View File

@ -1,31 +1,29 @@
Theming Element # Theming Element
============
Themes are a very basic way of providing simple alternative look & feels to the Themes are a very basic way of providing simple alternative look & feels to the
Element app via CSS & custom imagery. Element app via CSS & custom imagery.
They are *NOT* co be confused with 'skins', which describe apps which sit on top They are _NOT_ co be confused with 'skins', which describe apps which sit on top
of matrix-react-sdk - e.g. in theory Element itself is a react-sdk skin. of matrix-react-sdk - e.g. in theory Element itself is a react-sdk skin.
As of March 2022, skins are not fully supported; Element is the only available skin. As of March 2022, skins are not fully supported; Element is the only available skin.
To define a theme for Element: To define a theme for Element:
1. Pick a name, e.g. `teal`. at time of writing we have `light` and `dark`. 1. Pick a name, e.g. `teal`. at time of writing we have `light` and `dark`.
2. Fork `src/skins/vector/css/themes/dark.pcss` to be `teal.pcss` 2. Fork `src/skins/vector/css/themes/dark.pcss` to be `teal.pcss`
3. Fork `src/skins/vector/css/themes/_base.pcss` to be `_teal.pcss` 3. Fork `src/skins/vector/css/themes/_base.pcss` to be `_teal.pcss`
4. Override variables in `_teal.pcss` as desired. You may wish to delete ones 4. Override variables in `_teal.pcss` as desired. You may wish to delete ones
which don't differ from `_base.pcss`, to make it clear which are being which don't differ from `_base.pcss`, to make it clear which are being
overridden. If every single colour is being changed (as per `_dark.pcss`) overridden. If every single colour is being changed (as per `_dark.pcss`)
then you might as well keep them all. then you might as well keep them all.
5. Add the theme to the list of entrypoints in webpack.config.js 5. Add the theme to the list of entrypoints in webpack.config.js
6. Add the theme to the list of themes in matrix-react-sdk's UserSettings.js 6. Add the theme to the list of themes in matrix-react-sdk's UserSettings.js
7. Sit back and admire your handywork. 7. Sit back and admire your handywork.
In future, the assets for a theme will probably be gathered together into a In future, the assets for a theme will probably be gathered together into a
single directory tree. single directory tree.
Custom Themes # Custom Themes
=============
Themes derived from the built in themes may also be defined in settings. Themes derived from the built in themes may also be defined in settings.

View File

@ -2,11 +2,11 @@
## Requirements ## Requirements
- A working [Development Setup](../README.md#setting-up-a-dev-environment) - A working [Development Setup](../README.md#setting-up-a-dev-environment)
- Including up-to-date versions of matrix-react-sdk and matrix-js-sdk - Including up-to-date versions of matrix-react-sdk and matrix-js-sdk
- Latest LTS version of Node.js installed - Latest LTS version of Node.js installed
- Be able to understand English - Be able to understand English
- Be able to understand the language you want to translate Element into - Be able to understand the language you want to translate Element into
## Translating strings vs. marking strings for translation ## Translating strings vs. marking strings for translation
@ -15,6 +15,7 @@ Translating strings are done with the `_t()` function found in matrix-react-sdk/
Basically, whenever a translatable string is introduced, you should call either `_t()` immediately OR `_td()` and later `_t()`. Basically, whenever a translatable string is introduced, you should call either `_t()` immediately OR `_td()` and later `_t()`.
Example: Example:
``` ```
// Module-level constant // Module-level constant
const COLORS = { const COLORS = {
@ -30,10 +31,10 @@ function getColorName(hex) {
## Adding new strings ## Adding new strings
1. Check if the import ``import { _t } from 'matrix-react-sdk/src/languageHandler';`` is present. If not add it to the other import statements. Also import `_td` if needed. 1. Check if the import `import { _t } from 'matrix-react-sdk/src/languageHandler';` is present. If not add it to the other import statements. Also import `_td` if needed.
1. Add ``_t()`` to your string. (Don't forget curly braces when you assign an expression to JSX attributes in the render method). If the string is introduced at a point before the translation system has not yet been initialized, use `_td()` instead, and call `_t()` at the appropriate time. 1. Add `_t()` to your string. (Don't forget curly braces when you assign an expression to JSX attributes in the render method). If the string is introduced at a point before the translation system has not yet been initialized, use `_td()` instead, and call `_t()` at the appropriate time.
1. Run `yarn i18n` to update ``src/i18n/strings/en_EN.json`` 1. Run `yarn i18n` to update `src/i18n/strings/en_EN.json`
1. If you added a string with a plural, you can add other English plural variants to ``src/i18n/strings/en_EN.json`` (remeber to edit the one in the same project as the source file containing your new translation). 1. If you added a string with a plural, you can add other English plural variants to `src/i18n/strings/en_EN.json` (remeber to edit the one in the same project as the source file containing your new translation).
## Editing existing strings ## Editing existing strings
@ -43,21 +44,21 @@ function getColorName(hex) {
## Adding variables inside a string. ## Adding variables inside a string.
1. Extend your ``_t()`` call. Instead of ``_t(STRING)`` use ``_t(STRING, {})`` 1. Extend your `_t()` call. Instead of `_t(STRING)` use `_t(STRING, {})`
1. Decide how to name it. Please think about if the person who has to translate it can understand what it does. E.g. using the name 'recipient' is bad, because a translator does not know if it is the name of a person, an email address, a user ID, etc. Rather use e.g. recipientEmailAddress. 1. Decide how to name it. Please think about if the person who has to translate it can understand what it does. E.g. using the name 'recipient' is bad, because a translator does not know if it is the name of a person, an email address, a user ID, etc. Rather use e.g. recipientEmailAddress.
1. Add it to the array in ``_t`` for example ``_t(STRING, {variable: this.variable})`` 1. Add it to the array in `_t` for example `_t(STRING, {variable: this.variable})`
1. Add the variable inside the string. The syntax for variables is ``%(variable)s``. Please note the _s_ at the end. The name of the variable has to match the previous used name. 1. Add the variable inside the string. The syntax for variables is `%(variable)s`. Please note the _s_ at the end. The name of the variable has to match the previous used name.
- You can use the special ``count`` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. ``_t('You have %(count)s new messages', { count: 2 })`` would show 'You have 2 new messages', while ``_t('You have %(count)s new messages', { count: 1 })`` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in ``count`` is much prefered over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two). - You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much prefered over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two).
- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. ``_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })``. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink. - If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink.
- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. ``_t('Your email address is %(emailAddress)s', { emailAddress: <i>{userEmailAddress}</i> })``. - You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: <i>{userEmailAddress}</i> })`.
## Things to know/Style Guides ## Things to know/Style Guides
- Do not use `_t()` inside ``getDefaultProps``: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later. - Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later.
- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later. - If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later.
- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too. - If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too.
- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible. - Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible.
- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages. - Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages.
- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence. - Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence.
- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetion, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments. - If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetion, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments.

View File

@ -2,15 +2,15 @@
## Requirements ## Requirements
- Web Browser - Web Browser
- Be able to understand English - Be able to understand English
- Be able to understand the language you want to translate Element into - Be able to understand the language you want to translate Element into
## Step 0: Join #element-translations:matrix.org ## Step 0: Join #element-translations:matrix.org
1. Come and join https://matrix.to/#/#element-translations:matrix.org for general discussion 1. Come and join https://matrix.to/#/#element-translations:matrix.org for general discussion
2. Join https://matrix.to/#/#element-translators:matrix.org for language-specific rooms 2. Join https://matrix.to/#/#element-translators:matrix.org for language-specific rooms
3. Read scrollback and/or ask if anyone else is working on your language, and co-ordinate if needed. In general little-or-no coordination is needed though :) 3. Read scrollback and/or ask if anyone else is working on your language, and co-ordinate if needed. In general little-or-no coordination is needed though :)
## Step 1: Preparing your Weblate Profile ## Step 1: Preparing your Weblate Profile
@ -27,7 +27,7 @@ If your language is listed go to Step 2a and if not go to Step 2b
## Step 2a: Helping on existing languages. ## Step 2a: Helping on existing languages.
1. Head to one of the projects listed https://translate.element.io/projects/element-web/ 1. Head to one of the projects listed https://translate.element.io/projects/element-web/
2. Click on the ``translate`` button on the right side of your language 2. Click on the `translate` button on the right side of your language
3. Fill in the translations in the writeable field. You will see the original English string and the string of your second language above. 3. Fill in the translations in the writeable field. You will see the original English string and the string of your second language above.
Head to the explanations under Steb 2b Head to the explanations under Steb 2b
@ -35,7 +35,7 @@ Head to the explanations under Steb 2b
## Step 2b: Adding a new language ## Step 2b: Adding a new language
1. Go to one of the projects listed https://translate.element.io/projects/element-web/ 1. Go to one of the projects listed https://translate.element.io/projects/element-web/
2. Click the ``Start new translation`` button at the bottom 2. Click the `Start new translation` button at the bottom
3. Select a language 3. Select a language
4. Start translating like in 2a.3 4. Start translating like in 2a.3
5. Repeat these steps for the other projects which are listed at the link of step 2b.1 5. Repeat these steps for the other projects which are listed at the link of step 2b.1
@ -52,13 +52,12 @@ The yellow button has to be used if you are unsure about the translation but you
These things are variables that are expanded when displayed by Element. They can be room names, usernames or similar. If you find one, you can move to the right place for your language, but not delete it as the variable will be missing if you do. These things are variables that are expanded when displayed by Element. They can be room names, usernames or similar. If you find one, you can move to the right place for your language, but not delete it as the variable will be missing if you do.
A special case is `%(urlStart)s` and `%(urlEnd)s` which are used to mark the beginning of a hyperlink (i.e. `<a href="/somewhere">` and `</a>`. You must keep these markers surrounding the equivalent string in your language that needs to be hyperlinked. A special case is `%(urlStart)s` and `%(urlEnd)s` which are used to mark the beginning of a hyperlink (i.e. `<a href="/somewhere">` and `</a>`. You must keep these markers surrounding the equivalent string in your language that needs to be hyperlinked.
### "I want to come back to this string. How?" ### "I want to come back to this string. How?"
You can use inside the translation field "Review needed" checkbox. It will be shown as Strings that need to be reviewed. You can use inside the translation field "Review needed" checkbox. It will be shown as Strings that need to be reviewed.
### Further reading ### Further reading
The official Weblate doc provides some more in-depth explanation on how to do translations and talks about do and don'ts. You can find it at: https://docs.weblate.org/en/latest/user/translating.html The official Weblate doc provides some more in-depth explanation on how to do translations and talks about do and don'ts. You can find it at: https://docs.weblate.org/en/latest/user/translating.html

View File

@ -15,11 +15,7 @@
"uisi_autorageshake_app": "element-auto-uisi", "uisi_autorageshake_app": "element-auto-uisi",
"showLabsSettings": false, "showLabsSettings": false,
"roomDirectory": { "roomDirectory": {
"servers": [ "servers": ["matrix.org", "gitter.im", "libera.chat"]
"matrix.org",
"gitter.im",
"libera.chat"
]
}, },
"enable_presence_by_hs_url": { "enable_presence_by_hs_url": {
"https://matrix.org": false, "https://matrix.org": false,
@ -36,14 +32,12 @@
} }
], ],
"hostSignup": { "hostSignup": {
"brand": "Element Home", "brand": "Element Home",
"cookiePolicyUrl": "https://element.io/cookie-policy", "cookiePolicyUrl": "https://element.io/cookie-policy",
"domains": [ "domains": ["matrix.org"],
"matrix.org" "privacyPolicyUrl": "https://element.io/privacy",
], "termsOfServiceUrl": "https://element.io/terms-of-service",
"privacyPolicyUrl": "https://element.io/privacy", "url": "https://ems.element.io/element-home/in-app-loader"
"termsOfServiceUrl": "https://element.io/terms-of-service",
"url": "https://ems.element.io/element-home/in-app-loader"
}, },
"posthog": { "posthog": {
"projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", "projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",

View File

@ -15,11 +15,7 @@
"uisi_autorageshake_app": "element-auto-uisi", "uisi_autorageshake_app": "element-auto-uisi",
"showLabsSettings": true, "showLabsSettings": true,
"roomDirectory": { "roomDirectory": {
"servers": [ "servers": ["matrix.org", "gitter.im", "libera.chat"]
"matrix.org",
"gitter.im",
"libera.chat"
]
}, },
"enable_presence_by_hs_url": { "enable_presence_by_hs_url": {
"https://matrix.org": false, "https://matrix.org": false,
@ -36,14 +32,12 @@
} }
], ],
"hostSignup": { "hostSignup": {
"brand": "Element Home", "brand": "Element Home",
"cookiePolicyUrl": "https://element.io/cookie-policy", "cookiePolicyUrl": "https://element.io/cookie-policy",
"domains": [ "domains": ["matrix.org"],
"matrix.org" "privacyPolicyUrl": "https://element.io/privacy",
], "termsOfServiceUrl": "https://element.io/terms-of-service",
"privacyPolicyUrl": "https://element.io/privacy", "url": "https://ems.element.io/element-home/in-app-loader"
"termsOfServiceUrl": "https://element.io/terms-of-service",
"url": "https://ems.element.io/element-home/in-app-loader"
}, },
"sentry": { "sentry": {
"dsn": "https://029a0eb289f942508ae0fb17935bd8c5@sentry.matrix.org/6", "dsn": "https://029a0eb289f942508ae0fb17935bd8c5@sentry.matrix.org/6",

View File

@ -65,7 +65,7 @@ export function installer(config: BuildConfig): void {
// else must be a module, we assume. // else must be a module, we assume.
const pkgJsonStr = fs.readFileSync("./package.json", "utf-8"); const pkgJsonStr = fs.readFileSync("./package.json", "utf-8");
const optionalDepNames = getOptionalDepNames(pkgJsonStr); const optionalDepNames = getOptionalDepNames(pkgJsonStr);
const installedModules = optionalDepNames.filter(d => !currentOptDeps.includes(d)); const installedModules = optionalDepNames.filter((d) => !currentOptDeps.includes(d));
// Ensure all the modules are compatible. We check them all and report at the end to // Ensure all the modules are compatible. We check them all and report at the end to
// try and save the user some time debugging this sort of failure. // try and save the user some time debugging this sort of failure.
@ -80,7 +80,7 @@ export function installer(config: BuildConfig): void {
if (incompatibleNames.length > 0) { if (incompatibleNames.length > 0) {
console.error( console.error(
"The following modules are not compatible with this version of element-web. Please update the module " + "The following modules are not compatible with this version of element-web. Please update the module " +
"references and try again.", "references and try again.",
JSON.stringify(incompatibleNames, null, 4), // stringify to get prettier/complete output JSON.stringify(incompatibleNames, null, 4), // stringify to get prettier/complete output
); );
exitCode = 1; exitCode = 1;
@ -133,29 +133,33 @@ function callYarnAdd(dep: string): void {
// goes wrong in restoring the original package details. // goes wrong in restoring the original package details.
childProcess.execSync(`yarn add -O ${dep}`, { childProcess.execSync(`yarn add -O ${dep}`, {
env: process.env, env: process.env,
stdio: ['inherit', 'inherit', 'inherit'], stdio: ["inherit", "inherit", "inherit"],
}); });
} }
function getOptionalDepNames(pkgJsonStr: string): string[] { function getOptionalDepNames(pkgJsonStr: string): string[] {
return Object.keys(JSON.parse(pkgJsonStr)?.['optionalDependencies'] ?? {}); return Object.keys(JSON.parse(pkgJsonStr)?.["optionalDependencies"] ?? {});
} }
function findDepVersionInPackageJson(dep: string, pkgJsonStr: string): string { function findDepVersionInPackageJson(dep: string, pkgJsonStr: string): string {
const pkgJson = JSON.parse(pkgJsonStr); const pkgJson = JSON.parse(pkgJsonStr);
const packages = { const packages = {
...(pkgJson['optionalDependencies'] ?? {}), ...(pkgJson["optionalDependencies"] ?? {}),
...(pkgJson['devDependencies'] ?? {}), ...(pkgJson["devDependencies"] ?? {}),
...(pkgJson['dependencies'] ?? {}), ...(pkgJson["dependencies"] ?? {}),
}; };
return packages[dep]; return packages[dep];
} }
function getTopLevelDependencyVersion(dep: string): string { function getTopLevelDependencyVersion(dep: string): string {
const dependencyTree = JSON.parse(childProcess.execSync(`npm list ${dep} --depth=0 --json`, { const dependencyTree = JSON.parse(
env: process.env, childProcess
stdio: ['inherit', 'pipe', 'pipe'], .execSync(`npm list ${dep} --depth=0 --json`, {
}).toString('utf-8')); env: process.env,
stdio: ["inherit", "pipe", "pipe"],
})
.toString("utf-8"),
);
/* /*
What a dependency tree looks like: What a dependency tree looks like:

View File

@ -1,220 +1,220 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.11.16", "version": "1.11.16",
"description": "A feature-rich client for Matrix.org", "description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/vector-im/element-web" "url": "https://github.com/vector-im/element-web"
},
"license": "Apache-2.0",
"files": [
"lib",
"res",
"src",
"webpack.config.js",
"scripts",
"docs",
"release.sh",
"deploy",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"AUTHORS.rst",
"package.json",
"contribute.json"
],
"style": "bundle.css",
"scripts": {
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"clean": "rimraf lib webapp",
"build": "yarn clean && yarn build:genfiles && yarn build:bundle",
"build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
"build:jitsi": "node scripts/build-jitsi.js",
"build:res": "node scripts/copy-res.js",
"build:genfiles": "yarn build:res && yarn build:jitsi && yarn build:module_system",
"build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
"build:bundle": "webpack --progress --bail --mode production",
"build:bundle-stats": "webpack --progress --bail --mode production --json > webpack-stats.json",
"build:module_system": "tsc --project ./tsconfig.module_system.json && node ./lib/module_system/scripts/install.js",
"dist": "scripts/package.sh",
"start": "yarn build:module_system && concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js\"",
"start:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --https\"",
"start:res": "yarn build:jitsi && node scripts/copy-res.js -w",
"start:js": "webpack-dev-server --host=0.0.0.0 --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js -w --mode development --disable-host-check --hot",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"lint:js": "eslint --max-warnings 0 src module_system test && prettier --check .",
"lint:js-fix": "prettier --write . && eslint --fix src module_system test",
"lint:types": "tsc --noEmit --jsx react && tsc --noEmit --project ./tsconfig.module_system.json",
"lint:style": "stylelint \"res/css/**/*.pcss\"",
"test": "jest",
"coverage": "yarn test --coverage",
"analyse:unused-exports": "node ./scripts/analyse_unused_exports.js"
},
"dependencies": {
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
"@matrix-org/react-sdk-module-api": "^0.0.3",
"gfm.css": "^1.1.2",
"jsrsasign": "^10.5.25",
"katex": "^0.16.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#develop",
"matrix-widget-api": "^1.1.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"sanitize-html": "^2.3.2",
"ua-parser-js": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "^2.2.5",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^1.18.1",
"@svgr/webpack": "^5.5.0",
"@testing-library/react": "^12.1.5",
"@types/flux": "^3.1.9",
"@types/jest": "^29.0.0",
"@types/jsrsasign": "^10.5.4",
"@types/modernizr": "^3.5.3",
"@types/node": "^16",
"@types/react": "17.0.49",
"@types/react-dom": "17.0.17",
"@types/sanitize-html": "^2.3.1",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babel-loader": "^8.2.2",
"chokidar": "^3.5.1",
"concurrently": "^7.0.0",
"cpx": "^1.5.0",
"css-loader": "^4",
"dotenv": "^16.0.2",
"eslint": "8.28.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-deprecate": "^0.7.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.9.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"fake-indexeddb": "^4.0.0",
"fetch-mock-jest": "^1.5.1",
"file-loader": "^6.0.0",
"fs-extra": "^11.0.0",
"html-webpack-plugin": "^4.5.2",
"jest": "^29.0.0",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom": "^29.0.0",
"jest-mock": "^29.0.0",
"jest-raw-loader": "^1.0.1",
"json-loader": "^0.5.7",
"loader-utils": "^3.0.0",
"matrix-mock-request": "^2.5.0",
"matrix-web-i18n": "^1.3.0",
"mini-css-extract-plugin": "^1",
"minimist": "^1.2.6",
"mkdirp": "^1.0.4",
"modernizr": "^3.12.0",
"node-fetch": "^2.6.7",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss": "^8.4.16",
"postcss-easings": "^2.0.0",
"postcss-hexrgba": "2.0.1",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-mixins": "^6.2.3",
"postcss-nested": "^4.2.3",
"postcss-preset-env": "^6.7.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^5.0.2",
"prettier": "2.8.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"semver": "^7.3.7",
"simple-proxy-agent": "^1.1.0",
"string-replace-loader": "3",
"style-loader": "2",
"stylelint": "^14.9.1",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-standard": "^29.0.0",
"stylelint-scss": "^4.2.0",
"terser-webpack-plugin": "^4.0.0",
"ts-prune": "^0.10.3",
"typescript": "4.9.3",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2",
"worker-loader": "^2.0.0",
"worklet-loader": "^2.0.0",
"yaml": "^2.0.1"
},
"jest": {
"testEnvironment": "jsdom",
"testEnvironmentOptions": {
"url": "http://localhost/"
}, },
"testMatch": [ "license": "Apache-2.0",
"<rootDir>/test/**/*-test.[tj]s?(x)" "files": [
"lib",
"res",
"src",
"webpack.config.js",
"scripts",
"docs",
"release.sh",
"deploy",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"AUTHORS.rst",
"package.json",
"contribute.json"
], ],
"setupFiles": [ "style": "bundle.css",
"jest-canvas-mock" "scripts": {
], "i18n": "matrix-gen-i18n",
"setupFilesAfterEnv": [ "prunei18n": "matrix-prune-i18n",
"<rootDir>/node_modules/matrix-react-sdk/test/setupTests.js" "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
], "clean": "rimraf lib webapp",
"moduleNameMapper": { "build": "yarn clean && yarn build:genfiles && yarn build:bundle",
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js", "build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
"\\.(gif|png|ttf|woff2)$": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/imageMock.js", "build:jitsi": "node scripts/build-jitsi.js",
"\\.svg$": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/svg.js", "build:res": "node scripts/copy-res.js",
"\\$webapp/i18n/languages.json": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/languages.json", "build:genfiles": "yarn build:res && yarn build:jitsi && yarn build:module_system",
"^react$": "<rootDir>/node_modules/react", "build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
"^react-dom$": "<rootDir>/node_modules/react-dom", "build:bundle": "webpack --progress --bail --mode production",
"^matrix-js-sdk$": "<rootDir>/node_modules/matrix-js-sdk/src", "build:bundle-stats": "webpack --progress --bail --mode production --json > webpack-stats.json",
"^matrix-react-sdk$": "<rootDir>/node_modules/matrix-react-sdk/src", "build:module_system": "tsc --project ./tsconfig.module_system.json && node ./lib/module_system/scripts/install.js",
"decoderWorker\\.min\\.js": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js", "dist": "scripts/package.sh",
"decoderWorker\\.min\\.wasm": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js", "start": "yarn build:module_system && concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js\"",
"waveWorker\\.min\\.js": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js", "start:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --https\"",
"context-filter-polyfill": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js", "start:res": "yarn build:jitsi && node scripts/copy-res.js -w",
"FontManager.ts": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/FontManager.js", "start:js": "webpack-dev-server --host=0.0.0.0 --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js -w --mode development --disable-host-check --hot",
"workers/(.+)\\.worker\\.ts": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/workerMock.js", "lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"^!!raw-loader!.*": "jest-raw-loader", "lint:js": "eslint --max-warnings 0 src module_system test && prettier --check .",
"RecorderWorklet": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js" "lint:js-fix": "prettier --write . && eslint --fix src module_system test",
"lint:types": "tsc --noEmit --jsx react && tsc --noEmit --project ./tsconfig.module_system.json",
"lint:style": "stylelint \"res/css/**/*.pcss\"",
"test": "jest",
"coverage": "yarn test --coverage",
"analyse:unused-exports": "node ./scripts/analyse_unused_exports.js"
}, },
"transformIgnorePatterns": [ "dependencies": {
"/node_modules/(?!matrix-js-sdk).+$", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
"/node_modules/(?!matrix-react-sdk).+$" "@matrix-org/react-sdk-module-api": "^0.0.3",
], "gfm.css": "^1.1.2",
"coverageReporters": [ "jsrsasign": "^10.5.25",
"text-summary", "katex": "^0.16.0",
"lcov" "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
], "matrix-react-sdk": "github:matrix-org/matrix-react-sdk#develop",
"testResultsProcessor": "@casualbot/jest-sonar-reporter" "matrix-widget-api": "^1.1.1",
}, "react": "17.0.2",
"@casualbot/jest-sonar-reporter": { "react-dom": "17.0.2",
"outputDirectory": "coverage", "sanitize-html": "^2.3.2",
"outputName": "jest-sonar-report.xml", "ua-parser-js": "^1.0.0"
"relativePaths": true },
} "devDependencies": {
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "^2.2.5",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^1.18.1",
"@svgr/webpack": "^5.5.0",
"@testing-library/react": "^12.1.5",
"@types/flux": "^3.1.9",
"@types/jest": "^29.0.0",
"@types/jsrsasign": "^10.5.4",
"@types/modernizr": "^3.5.3",
"@types/node": "^16",
"@types/react": "17.0.49",
"@types/react-dom": "17.0.17",
"@types/sanitize-html": "^2.3.1",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babel-loader": "^8.2.2",
"chokidar": "^3.5.1",
"concurrently": "^7.0.0",
"cpx": "^1.5.0",
"css-loader": "^4",
"dotenv": "^16.0.2",
"eslint": "8.28.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-deprecate": "^0.7.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.9.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"fake-indexeddb": "^4.0.0",
"fetch-mock-jest": "^1.5.1",
"file-loader": "^6.0.0",
"fs-extra": "^11.0.0",
"html-webpack-plugin": "^4.5.2",
"jest": "^29.0.0",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom": "^29.0.0",
"jest-mock": "^29.0.0",
"jest-raw-loader": "^1.0.1",
"json-loader": "^0.5.7",
"loader-utils": "^3.0.0",
"matrix-mock-request": "^2.5.0",
"matrix-web-i18n": "^1.3.0",
"mini-css-extract-plugin": "^1",
"minimist": "^1.2.6",
"mkdirp": "^1.0.4",
"modernizr": "^3.12.0",
"node-fetch": "^2.6.7",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss": "^8.4.16",
"postcss-easings": "^2.0.0",
"postcss-hexrgba": "2.0.1",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-mixins": "^6.2.3",
"postcss-nested": "^4.2.3",
"postcss-preset-env": "^6.7.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^5.0.2",
"prettier": "2.8.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"semver": "^7.3.7",
"simple-proxy-agent": "^1.1.0",
"string-replace-loader": "3",
"style-loader": "2",
"stylelint": "^14.9.1",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-standard": "^29.0.0",
"stylelint-scss": "^4.2.0",
"terser-webpack-plugin": "^4.0.0",
"ts-prune": "^0.10.3",
"typescript": "4.9.3",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2",
"worker-loader": "^2.0.0",
"worklet-loader": "^2.0.0",
"yaml": "^2.0.1"
},
"jest": {
"testEnvironment": "jsdom",
"testEnvironmentOptions": {
"url": "http://localhost/"
},
"testMatch": [
"<rootDir>/test/**/*-test.[tj]s?(x)"
],
"setupFiles": [
"jest-canvas-mock"
],
"setupFilesAfterEnv": [
"<rootDir>/node_modules/matrix-react-sdk/test/setupTests.js"
],
"moduleNameMapper": {
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
"\\.(gif|png|ttf|woff2)$": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/svg.js",
"\\$webapp/i18n/languages.json": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/languages.json",
"^react$": "<rootDir>/node_modules/react",
"^react-dom$": "<rootDir>/node_modules/react-dom",
"^matrix-js-sdk$": "<rootDir>/node_modules/matrix-js-sdk/src",
"^matrix-react-sdk$": "<rootDir>/node_modules/matrix-react-sdk/src",
"decoderWorker\\.min\\.js": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js",
"decoderWorker\\.min\\.wasm": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js",
"waveWorker\\.min\\.js": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js",
"context-filter-polyfill": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js",
"FontManager.ts": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/FontManager.js",
"workers/(.+)\\.worker\\.ts": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/workerMock.js",
"^!!raw-loader!.*": "jest-raw-loader",
"RecorderWorklet": "<rootDir>/node_modules/matrix-react-sdk/__mocks__/empty.js"
},
"transformIgnorePatterns": [
"/node_modules/(?!matrix-js-sdk).+$",
"/node_modules/(?!matrix-react-sdk).+$"
],
"coverageReporters": [
"text-summary",
"lcov"
],
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
}
} }

View File

@ -25,17 +25,8 @@ limitations under the License.
background: linear-gradient(to bottom, #c5e0f7 0%, #ffffff 100%); background: linear-gradient(to bottom, #c5e0f7 0%, #ffffff 100%);
/* stylelint-disable-next-line function-no-unknown */ /* stylelint-disable-next-line function-no-unknown */
filter: progid:dximagetransform.microsoft.gradient(startColorstr='#c5e0f7', endColorstr='#ffffff', GradientType=0); filter: progid:dximagetransform.microsoft.gradient(startColorstr='#c5e0f7', endColorstr='#ffffff', GradientType=0);
font-family: font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
-apple-system, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
color: #000; color: #000;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -99,7 +90,8 @@ limitations under the License.
margin: auto 20px auto 0; margin: auto 20px auto 0;
} }
h1, h2 { h1,
h2 {
font-weight: 600; font-weight: 600;
margin-bottom: 32px; margin-bottom: 32px;
} }

View File

@ -20,7 +20,7 @@
class Optional { class Optional {
static from(value) { static from(value) {
return value && Some.of(value) || None; return (value && Some.of(value)) || None;
} }
map(f) { map(f) {
return this; return this;

View File

@ -10,11 +10,11 @@ async function getBundleName(baseUrl) {
throw new StartupError(`Couldn't fetch index.html to prefill bundle; ${res.status} ${res.statusText}`); throw new StartupError(`Couldn't fetch index.html to prefill bundle; ${res.status} ${res.statusText}`);
} }
const index = await res.text(); const index = await res.text();
return index.split("\n").map((line) => return index
line.match(/<script src="bundles\/([^/]+)\/bundle.js"/), .split("\n")
) .map((line) => line.match(/<script src="bundles\/([^/]+)\/bundle.js"/))
.filter((result) => result) .filter((result) => result)
.map((result) => result[1])[0]; .map((result) => result[1])[0];
} }
function validateBundle(value) { function validateBundle(value) {
@ -69,7 +69,7 @@ function observeReadableStream(readableStream, pendingContext = {}) {
return; return;
} }
bytesReceived += value.length; bytesReceived += value.length;
pendingSubject.next(Pending.of({...pendingContext, bytesReceived })); pendingSubject.next(Pending.of({ ...pendingContext, bytesReceived }));
/* string concatenation is apparently the most performant way to do this */ /* string concatenation is apparently the most performant way to do this */
buffer += utf8Decoder.decode(value); buffer += utf8Decoder.decode(value);
readNextChunk(); readNextChunk();
@ -120,25 +120,27 @@ const e = React.createElement;
* Provides user feedback given a FetchStatus object. * Provides user feedback given a FetchStatus object.
*/ */
function ProgressBar({ fetchStatus }) { function ProgressBar({ fetchStatus }) {
return e('span', { className: "progress "}, return e(
"span",
{ className: "progress " },
fetchStatus.fold({ fetchStatus.fold({
pending: ({ bytesReceived, length }) => { pending: ({ bytesReceived, length }) => {
if (!bytesReceived) { if (!bytesReceived) {
return e('span', { className: "spinner" }, "\u29b5"); return e("span", { className: "spinner" }, "\u29b5");
} }
const kB = Math.floor(10 * bytesReceived / 1024) / 10; const kB = Math.floor((10 * bytesReceived) / 1024) / 10;
if (!length) { if (!length) {
return e('span', null, `Fetching (${kB}kB)`); return e("span", null, `Fetching (${kB}kB)`);
} }
const percent = Math.floor(100 * bytesReceived / length); const percent = Math.floor((100 * bytesReceived) / length);
return e('span', null, `Fetching (${kB}kB) ${percent}%`); return e("span", null, `Fetching (${kB}kB) ${percent}%`);
}, },
success: () => e('span', null, "\u2713"), success: () => e("span", null, "\u2713"),
error: (reason) => { error: (reason) => {
return e('span', { className: 'error'}, `\u2717 ${reason}`); return e("span", { className: "error" }, `\u2717 ${reason}`);
}, },
}, }),
)); );
} }
/* /*
@ -193,23 +195,24 @@ function BundlePicker() {
setColumn(value); setColumn(value);
}, []); }, []);
/* ------------------------------------------------ */ /* ------------------------------------------------ */
/* Plumb data-fetching observables through to React */ /* Plumb data-fetching observables through to React */
/* ------------------------------------------------ */ /* ------------------------------------------------ */
/* Whenever a valid bundle name is input, go see if it's a real bundle on the server */ /* Whenever a valid bundle name is input, go see if it's a real bundle on the server */
React.useEffect(() => React.useEffect(
validateBundle(bundle).fold({ () =>
some: (value) => { validateBundle(bundle).fold({
const subscription = bundleSubject(baseUrl, value) some: (value) => {
.pipe(rxjs.operators.map(Some.of)) const subscription = bundleSubject(baseUrl, value)
.subscribe(setBundleFetchStatus); .pipe(rxjs.operators.map(Some.of))
return () => subscription.unsubscribe(); .subscribe(setBundleFetchStatus);
}, return () => subscription.unsubscribe();
none: () => setBundleFetchStatus(None), },
}), none: () => setBundleFetchStatus(None),
[baseUrl, bundle]); }),
[baseUrl, bundle],
);
/* Whenever a valid javascript file is input, see if it corresponds to a sourcemap file and initiate a fetch /* Whenever a valid javascript file is input, see if it corresponds to a sourcemap file and initiate a fetch
* if so. */ * if so. */
@ -218,17 +221,18 @@ function BundlePicker() {
setFileFetchStatus(None); setFileFetchStatus(None);
return; return;
} }
const observable = fetchAsSubject(new URL(`bundles/${bundle}/${file}.map`, baseUrl).toString()) const observable = fetchAsSubject(new URL(`bundles/${bundle}/${file}.map`, baseUrl).toString()).pipe(
.pipe( rxjs.operators.map((fetchStatus) =>
rxjs.operators.map((fetchStatus) => fetchStatus.flatMap(value => { fetchStatus.flatMap((value) => {
try { try {
return Success.of(JSON.parse(value)); return Success.of(JSON.parse(value));
} catch (e) { } catch (e) {
return FetchError.of(e); return FetchError.of(e);
} }
})), }),
rxjs.operators.map(Some.of), ),
); rxjs.operators.map(Some.of),
);
const subscription = observable.subscribe(setFileFetchStatus); const subscription = observable.subscribe(setFileFetchStatus);
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, [baseUrl, bundle, file]); }, [baseUrl, bundle, file]);
@ -256,26 +260,33 @@ function BundlePicker() {
}); });
}, [fileFetchStatus, line, column]); }, [fileFetchStatus, line, column]);
/* ------ */ /* ------ */
/* Render */ /* Render */
/* ------ */ /* ------ */
return e('div', {}, return e(
e('div', { className: 'inputs' }, "div",
e('div', { className: 'baseUrl' }, {},
e('label', { htmlFor: 'baseUrl'}, 'Base URL'), e(
e('input', { "div",
name: 'baseUrl', { className: "inputs" },
e(
"div",
{ className: "baseUrl" },
e("label", { htmlFor: "baseUrl" }, "Base URL"),
e("input", {
name: "baseUrl",
required: true, required: true,
pattern: ".+", pattern: ".+",
onChange: onBaseUrlChange, onChange: onBaseUrlChange,
value: baseUrl, value: baseUrl,
}), }),
), ),
e('div', { className: 'bundle' }, e(
e('label', { htmlFor: 'bundle'}, 'Bundle'), "div",
e('input', { { className: "bundle" },
name: 'bundle', e("label", { htmlFor: "bundle" }, "Bundle"),
e("input", {
name: "bundle",
required: true, required: true,
pattern: "[0-9a-f]{20}", pattern: "[0-9a-f]{20}",
onChange: onBundleChange, onChange: onBundleChange,
@ -286,10 +297,12 @@ function BundlePicker() {
none: () => null, none: () => null,
}), }),
), ),
e('div', { className: 'file' }, e(
e('label', { htmlFor: 'file' }, 'File'), "div",
e('input', { { className: "file" },
name: 'file', e("label", { htmlFor: "file" }, "File"),
e("input", {
name: "file",
required: true, required: true,
pattern: ".+\\.js", pattern: ".+\\.js",
onChange: onFileChange, onChange: onFileChange,
@ -300,20 +313,24 @@ function BundlePicker() {
none: () => null, none: () => null,
}), }),
), ),
e('div', { className: 'line' }, e(
e('label', { htmlFor: 'line' }, 'Line'), "div",
e('input', { { className: "line" },
name: 'line', e("label", { htmlFor: "line" }, "Line"),
e("input", {
name: "line",
required: true, required: true,
pattern: "[0-9]+", pattern: "[0-9]+",
onChange: onLineChange, onChange: onLineChange,
value: line, value: line,
}), }),
), ),
e('div', { className: 'column' }, e(
e('label', { htmlFor: 'column' }, 'Column'), "div",
e('input', { { className: "column" },
name: 'column', e("label", { htmlFor: "column" }, "Column"),
e("input", {
name: "column",
required: true, required: true,
pattern: "[0-9]+", pattern: "[0-9]+",
onChange: onColumnChange, onChange: onColumnChange,
@ -321,10 +338,12 @@ function BundlePicker() {
}), }),
), ),
), ),
e('div', null, e(
"div",
null,
result.fold({ result.fold({
none: () => "Select a bundle, file and line", none: () => "Select a bundle, file and line",
some: (value) => e('pre', null, value), some: (value) => e("pre", null, value),
}), }),
), ),
); );

View File

@ -1,11 +1,11 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Rageshake decoder ring</title> <title>Rageshake decoder ring</title>
<script crossorigin src="https://unpkg.com/source-map@0.7.3/dist/source-map.js"></script> <script crossorigin src="https://unpkg.com/source-map@0.7.3/dist/source-map.js"></script>
<script> <script>
sourceMap.SourceMapConsumer.initialize({ sourceMap.SourceMapConsumer.initialize({
"lib/mappings.wasm": "https://unpkg.com/source-map@0.7.3/lib/mappings.wasm" "lib/mappings.wasm": "https://unpkg.com/source-map@0.7.3/lib/mappings.wasm",
}); });
</script> </script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
@ -18,12 +18,16 @@
<style> <style>
@keyframes spin { @keyframes spin {
from {transform:rotate(0deg);} from {
to {transform:rotate(359deg);} transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
} }
body { body {
font-family: sans-serif font-family: sans-serif;
} }
.spinner { .spinner {
@ -44,7 +48,7 @@
} }
.valid::after { .valid::after {
content: "✓" content: "✓";
} }
label { label {
@ -68,7 +72,7 @@
<script type="text/javascript"> <script type="text/javascript">
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
try { try {
ReactDOM.render(React.createElement(Decoder.BundlePicker), document.getElementById("main")) ReactDOM.render(React.createElement(Decoder.BundlePicker), document.getElementById("main"));
} catch (e) { } catch (e) {
const n = document.createElement("div"); const n = document.createElement("div");
n.innerText = `Error starting: ${e.message}`; n.innerText = `Error starting: ${e.message}`;

View File

@ -1 +1 @@
self.addEventListener('fetch', () => {}); self.addEventListener("fetch", () => {});

View File

@ -1,175 +1,173 @@
<style type="text/css"> <style type="text/css">
/* we deliberately inline style here to avoid flash-of-CSS problems, and to avoid
/* we deliberately inline style here to avoid flash-of-CSS problems, and to avoid
* voodoo where we have to set display: none by default * voodoo where we have to set display: none by default
*/ */
h1::after { h1::after {
content: "!"; content: "!";
} }
.mx_Parent { .mx_Parent {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-box-direction: normal; -webkit-box-direction: normal;
-webkit-flex-direction: column; -webkit-flex-direction: column;
-ms-flex-direction: column; -ms-flex-direction: column;
flex-direction: column; flex-direction: column;
-webkit-box-pack: center; -webkit-box-pack: center;
-webkit-justify-content: center; -webkit-justify-content: center;
-ms-flex-pack: center; -ms-flex-pack: center;
justify-content: center; justify-content: center;
-webkit-box-align: center; -webkit-box-align: center;
-webkit-align-items: center; -webkit-align-items: center;
-ms-flex-align: center; -ms-flex-align: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
padding: 25px 35px; padding: 25px 35px;
color: #2e2f32; color: #2e2f32;
} }
.mx_Logo { .mx_Logo {
height: 54px; height: 54px;
margin-top: 2px; margin-top: 2px;
} }
.mx_ButtonGroup { .mx_ButtonGroup {
margin-top: 10px; margin-top: 10px;
} }
.mx_ButtonRow { .mx_ButtonRow {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-webkit-justify-content: space-around; -webkit-justify-content: space-around;
-ms-flex-pack: distribute; -ms-flex-pack: distribute;
-webkit-box-align: center; -webkit-box-align: center;
-webkit-align-items: center; -webkit-align-items: center;
-ms-flex-align: center; -ms-flex-align: center;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
box-sizing: border-box; box-sizing: border-box;
margin: 12px 0 0; margin: 12px 0 0;
} }
.mx_ButtonRow > * { .mx_ButtonRow > * {
margin: 0 10px; margin: 0 10px;
} }
.mx_ButtonRow > *:first-child { .mx_ButtonRow > *:first-child {
margin-left: 0; margin-left: 0;
} }
.mx_ButtonRow > *:last-child { .mx_ButtonRow > *:last-child {
margin-right: 0; margin-right: 0;
} }
.mx_ButtonParent { .mx_ButtonParent {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
padding: 10px 20px; padding: 10px 20px;
-webkit-box-orient: horizontal; -webkit-box-orient: horizontal;
-webkit-box-direction: normal; -webkit-box-direction: normal;
-webkit-flex-direction: row; -webkit-flex-direction: row;
-ms-flex-direction: row; -ms-flex-direction: row;
flex-direction: row; flex-direction: row;
-webkit-box-pack: center; -webkit-box-pack: center;
-webkit-justify-content: center; -webkit-justify-content: center;
-ms-flex-pack: center; -ms-flex-pack: center;
justify-content: center; justify-content: center;
-webkit-box-align: center; -webkit-box-align: center;
-webkit-align-items: center; -webkit-align-items: center;
-ms-flex-align: center; -ms-flex-align: center;
align-items: center; align-items: center;
border-radius: 4px; border-radius: 4px;
width: 150px; width: 150px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 10px center; background-position: 10px center;
text-decoration: none; text-decoration: none;
color: #2e2f32 !important; color: #2e2f32 !important;
} }
.mx_ButtonLabel { .mx_ButtonLabel {
margin-left: 20px; margin-left: 20px;
} }
.mx_Header_title { .mx_Header_title {
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
margin: 20px 0 0; margin: 20px 0 0;
} }
.mx_Header_subtitle { .mx_Header_subtitle {
font-size: 12px; font-size: 12px;
font-weight: normal; font-weight: normal;
margin: 8px 0 0; margin: 8px 0 0;
} }
.mx_ButtonSignIn { .mx_ButtonSignIn {
background-color: #368BD6; background-color: #368bd6;
color: white !important; color: white !important;
} }
.mx_ButtonCreateAccount { .mx_ButtonCreateAccount {
background-color: #0DBD8B; background-color: #0dbd8b;
color: white !important; color: white !important;
} }
.mx_SecondaryButton { .mx_SecondaryButton {
background-color: #FFFFFF; background-color: #ffffff;
color: #2E2F32; color: #2e2f32;
} }
.mx_Button_iconSignIn { .mx_Button_iconSignIn {
background-image: url('welcome/images/icon-sign-in.svg'); background-image: url("welcome/images/icon-sign-in.svg");
} }
.mx_Button_iconCreateAccount { .mx_Button_iconCreateAccount {
background-image: url('welcome/images/icon-create-account.svg'); background-image: url("welcome/images/icon-create-account.svg");
} }
.mx_Button_iconHelp { .mx_Button_iconHelp {
background-image: url('welcome/images/icon-help.svg'); background-image: url("welcome/images/icon-help.svg");
} }
.mx_Button_iconRoomDirectory { .mx_Button_iconRoomDirectory {
background-image: url('welcome/images/icon-room-directory.svg'); background-image: url("welcome/images/icon-room-directory.svg");
} }
/* /*
.mx_WelcomePage_loggedIn is applied by EmbeddedPage from the Welcome component .mx_WelcomePage_loggedIn is applied by EmbeddedPage from the Welcome component
If it is set on the page, we should show the buttons. Otherwise, we have to assume If it is set on the page, we should show the buttons. Otherwise, we have to assume
we don't have an account and should hide them. No account == no guest account either. we don't have an account and should hide them. No account == no guest account either.
*/ */
.mx_WelcomePage:not(.mx_WelcomePage_loggedIn) .mx_WelcomePage_guestFunctions { .mx_WelcomePage:not(.mx_WelcomePage_loggedIn) .mx_WelcomePage_guestFunctions {
display: none; display: none;
}
.mx_ButtonRow.mx_WelcomePage_guestFunctions {
margin-top: 20px;
}
.mx_ButtonRow.mx_WelcomePage_guestFunctions > div {
margin: 0 auto;
}
@media only screen and (max-width: 480px) {
.mx_ButtonRow {
flex-direction: column;
} }
.mx_ButtonRow > * { .mx_ButtonRow.mx_WelcomePage_guestFunctions {
margin: 0 0 10px 0; margin-top: 20px;
}
.mx_ButtonRow.mx_WelcomePage_guestFunctions > div {
margin: 0 auto;
} }
}
@media only screen and (max-width: 480px) {
.mx_ButtonRow {
flex-direction: column;
}
.mx_ButtonRow > * {
margin: 0 0 10px 0;
}
}
</style> </style>
<div class="mx_Parent"> <div class="mx_Parent">
<a href="https://element.io" target="_blank" rel="noopener"> <a href="https://element.io" target="_blank" rel="noopener">
<img src="welcome/images/logo.svg" alt="" class="mx_Logo"/> <img src="welcome/images/logo.svg" alt="" class="mx_Logo" />
</a> </a>
<h1 class="mx_Header_title">_t("Welcome to Element")</h1> <h1 class="mx_Header_title">_t("Welcome to Element")</h1>
<!-- XXX: Our translations system isn't smart enough to recognize variables in the HTML, so we manually do it --> <!-- XXX: Our translations system isn't smart enough to recognize variables in the HTML, so we manually do it -->

View File

@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; "use strict";
const fs = require("fs"); const fs = require("fs");
const { exec } = require("node:child_process"); const { exec } = require("node:child_process");

View File

@ -21,11 +21,13 @@ if (process.env.HTTPS_PROXY) {
options.agent = new ProxyAgent(process.env.HTTPS_PROXY, { tunnel: true }); options.agent = new ProxyAgent(process.env.HTTPS_PROXY, { tunnel: true });
} }
fetch("https://meet.element.io/libs/external_api.min.js", options).then(res => { fetch("https://meet.element.io/libs/external_api.min.js", options)
const stream = fs.createWriteStream(fname); .then((res) => {
return new Promise((resolve, reject) => { const stream = fs.createWriteStream(fname);
res.body.pipe(stream); return new Promise((resolve, reject) => {
res.body.on('error', err => reject(err)); res.body.pipe(stream);
res.body.on('finish', () => resolve()); res.body.on("error", (err) => reject(err));
}); res.body.on("finish", () => resolve());
}).then(() => console.log('Done with Jitsi download')); });
})
.then(() => console.log("Done with Jitsi download"));

View File

@ -11,52 +11,52 @@ const loaderUtils = require("loader-utils");
// This could readily be automated, but it's nice to explicitly // This could readily be automated, but it's nice to explicitly
// control when new languages are available. // control when new languages are available.
const INCLUDE_LANGS = [ const INCLUDE_LANGS = [
{'value': 'bg', 'label': 'Български'}, { value: "bg", label: "Български" },
{'value': 'ca', 'label': 'Català'}, { value: "ca", label: "Català" },
{'value': 'cs', 'label': 'čeština'}, { value: "cs", label: "čeština" },
{'value': 'da', 'label': 'Dansk'}, { value: "da", label: "Dansk" },
{'value': 'de_DE', 'label': 'Deutsch'}, { value: "de_DE", label: "Deutsch" },
{'value': 'el', 'label': 'Ελληνικά'}, { value: "el", label: "Ελληνικά" },
{'value': 'en_EN', 'label': 'English'}, { value: "en_EN", label: "English" },
{'value': 'en_US', 'label': 'English (US)'}, { value: "en_US", label: "English (US)" },
{'value': 'eo', 'label': 'Esperanto'}, { value: "eo", label: "Esperanto" },
{'value': 'es', 'label': 'Español'}, { value: "es", label: "Español" },
{'value': 'et', 'label': 'Eesti'}, { value: "et", label: "Eesti" },
{'value': 'eu', 'label': 'Euskara'}, { value: "eu", label: "Euskara" },
{'value': 'fi', 'label': 'Suomi'}, { value: "fi", label: "Suomi" },
{'value': 'fr', 'label': 'Français'}, { value: "fr", label: "Français" },
{'value': 'gl', 'label': 'Galego'}, { value: "gl", label: "Galego" },
{'value': 'he', 'label': 'עברית'}, { value: "he", label: "עברית" },
{'value': 'hi', 'label': 'हिन्दी'}, { value: "hi", label: "हिन्दी" },
{'value': 'hu', 'label': 'Magyar'}, { value: "hu", label: "Magyar" },
{'value': 'id', 'label': 'Bahasa Indonesia'}, { value: "id", label: "Bahasa Indonesia" },
{'value': 'is', 'label': 'íslenska'}, { value: "is", label: "íslenska" },
{'value': 'it', 'label': 'Italiano'}, { value: "it", label: "Italiano" },
{'value': 'ja', 'label': '日本語'}, { value: "ja", label: "日本語" },
{'value': 'kab', 'label': 'Taqbaylit'}, { value: "kab", label: "Taqbaylit" },
{'value': 'ko', 'label': '한국어'}, { value: "ko", label: "한국어" },
{'value': 'lo', 'label': 'ລາວ'}, { value: "lo", label: "ລາວ" },
{'value': 'lt', 'label': 'Lietuvių'}, { value: "lt", label: "Lietuvių" },
{'value': 'lv', 'label': 'Latviešu'}, { value: "lv", label: "Latviešu" },
{'value': 'nb_NO', 'label': 'Norwegian Bokmål'}, { value: "nb_NO", label: "Norwegian Bokmål" },
{'value': 'nl', 'label': 'Nederlands'}, { value: "nl", label: "Nederlands" },
{'value': 'nn', 'label': 'Norsk Nynorsk'}, { value: "nn", label: "Norsk Nynorsk" },
{'value': 'pl', 'label': 'Polski'}, { value: "pl", label: "Polski" },
{'value': 'pt', 'label': 'Português'}, { value: "pt", label: "Português" },
{'value': 'pt_BR', 'label': 'Português do Brasil'}, { value: "pt_BR", label: "Português do Brasil" },
{'value': 'ru', 'label': 'Русский'}, { value: "ru", label: "Русский" },
{'value': 'sk', 'label': 'Slovenčina'}, { value: "sk", label: "Slovenčina" },
{'value': 'sq', 'label': 'Shqip'}, { value: "sq", label: "Shqip" },
{'value': 'sr', 'label': 'српски'}, { value: "sr", label: "српски" },
{'value': 'sv', 'label': 'Svenska'}, { value: "sv", label: "Svenska" },
{'value': 'te', 'label': 'తెలుగు'}, { value: "te", label: "తెలుగు" },
{'value': 'th', 'label': 'ไทย'}, { value: "th", label: "ไทย" },
{'value': 'tr', 'label': 'Türkçe'}, { value: "tr", label: "Türkçe" },
{'value': 'uk', 'label': 'українська мова'}, { value: "uk", label: "українська мова" },
{'value': 'vi', 'label': 'Tiếng Việt'}, { value: "vi", label: "Tiếng Việt" },
{'value': 'vls', 'label': 'West-Vlaams'}, { value: "vls", label: "West-Vlaams" },
{'value': 'zh_Hans', 'label': '简体中文'}, // simplified chinese { value: "zh_Hans", label: "简体中文" }, // simplified chinese
{'value': 'zh_Hant', 'label': '繁體中文'}, // traditional chinese { value: "zh_Hant", label: "繁體中文" }, // traditional chinese
]; ];
// cpx includes globbed parts of the filename in the destination, but excludes // cpx includes globbed parts of the filename in the destination, but excludes
@ -77,15 +77,13 @@ const COPY_LIST = [
["contribute.json", "webapp"], ["contribute.json", "webapp"],
]; ];
const parseArgs = require('minimist'); const parseArgs = require("minimist");
const Cpx = require('cpx'); const Cpx = require("cpx");
const chokidar = require('chokidar'); const chokidar = require("chokidar");
const fs = require('fs'); const fs = require("fs");
const rimraf = require('rimraf'); const rimraf = require("rimraf");
const argv = parseArgs( const argv = parseArgs(process.argv.slice(2), {});
process.argv.slice(2), {}
);
const watch = argv.w; const watch = argv.w;
const verbose = argv.v; const verbose = argv.v;
@ -98,12 +96,12 @@ function errCheck(err) {
} }
// Check if webapp exists // Check if webapp exists
if (!fs.existsSync('webapp')) { if (!fs.existsSync("webapp")) {
fs.mkdirSync('webapp'); fs.mkdirSync("webapp");
} }
// Check if i18n exists // Check if i18n exists
if (!fs.existsSync('webapp/i18n/')) { if (!fs.existsSync("webapp/i18n/")) {
fs.mkdirSync('webapp/i18n/'); fs.mkdirSync("webapp/i18n/");
} }
function next(i, err) { function next(i, err) {
@ -132,7 +130,9 @@ function next(i, err) {
}); });
} }
const cb = (err) => { next(i + 1, err) }; const cb = (err) => {
next(i + 1, err);
};
if (watch) { if (watch) {
if (opts.directwatch) { if (opts.directwatch) {
@ -140,14 +140,12 @@ function next(i, err) {
// which in the case of config.json is '.', which inevitably takes // which in the case of config.json is '.', which inevitably takes
// ages to crawl. So we create our own watcher on the files // ages to crawl. So we create our own watcher on the files
// instead. // instead.
const copy = () => { cpx.copy(errCheck) }; const copy = () => {
chokidar.watch(source) cpx.copy(errCheck);
.on('add', copy) };
.on('change', copy) chokidar.watch(source).on("add", copy).on("change", copy).on("ready", cb).on("error", errCheck);
.on('ready', cb)
.on('error', errCheck);
} else { } else {
cpx.on('watch-ready', cb); cpx.on("watch-ready", cb);
cpx.on("watch-error", cb); cpx.on("watch-error", cb);
cpx.watch(); cpx.watch();
} }
@ -157,17 +155,14 @@ function next(i, err) {
} }
function genLangFile(lang, dest) { function genLangFile(lang, dest) {
const reactSdkFile = 'node_modules/matrix-react-sdk/src/i18n/strings/' + lang + '.json'; const reactSdkFile = "node_modules/matrix-react-sdk/src/i18n/strings/" + lang + ".json";
const riotWebFile = 'src/i18n/strings/' + lang + '.json'; const riotWebFile = "src/i18n/strings/" + lang + ".json";
let translations = {}; let translations = {};
[reactSdkFile, riotWebFile].forEach(function(f) { [reactSdkFile, riotWebFile].forEach(function (f) {
if (fs.existsSync(f)) { if (fs.existsSync(f)) {
try { try {
Object.assign( Object.assign(translations, JSON.parse(fs.readFileSync(f).toString()));
translations,
JSON.parse(fs.readFileSync(f).toString())
);
} catch (e) { } catch (e) {
console.error("Failed: " + f, e); console.error("Failed: " + f, e);
throw e; throw e;
@ -192,16 +187,16 @@ function genLangFile(lang, dest) {
function genLangList(langFileMap) { function genLangList(langFileMap) {
const languages = {}; const languages = {};
INCLUDE_LANGS.forEach(function(lang) { INCLUDE_LANGS.forEach(function (lang) {
const normalizedLanguage = lang.value.toLowerCase().replace("_", "-"); const normalizedLanguage = lang.value.toLowerCase().replace("_", "-");
const languageParts = normalizedLanguage.split('-'); const languageParts = normalizedLanguage.split("-");
if (languageParts.length == 2 && languageParts[0] == languageParts[1]) { if (languageParts.length == 2 && languageParts[0] == languageParts[1]) {
languages[languageParts[0]] = {'fileName': langFileMap[lang.value], 'label': lang.label}; languages[languageParts[0]] = { fileName: langFileMap[lang.value], label: lang.label };
} else { } else {
languages[normalizedLanguage] = {'fileName': langFileMap[lang.value], 'label': lang.label}; languages[normalizedLanguage] = { fileName: langFileMap[lang.value], label: lang.label };
} }
}); });
fs.writeFile('webapp/i18n/languages.json', JSON.stringify(languages, null, 4), function(err) { fs.writeFile("webapp/i18n/languages.json", JSON.stringify(languages, null, 4), function (err) {
if (err) { if (err) {
console.error("Copy Error occured: " + err); console.error("Copy Error occured: " + err);
throw new Error("Failed to generate languages.json"); throw new Error("Failed to generate languages.json");
@ -230,7 +225,7 @@ function weblateToCounterpart(inTrs) {
const outTrs = {}; const outTrs = {};
for (const key of Object.keys(inTrs)) { for (const key of Object.keys(inTrs)) {
const keyParts = key.split('|', 2); const keyParts = key.split("|", 2);
if (keyParts.length === 2) { if (keyParts.length === 2) {
let obj = outTrs[keyParts[0]]; let obj = outTrs[keyParts[0]];
if (obj === undefined) { if (obj === undefined) {
@ -239,7 +234,7 @@ function weblateToCounterpart(inTrs) {
// This is a transitional edge case if a string went from singular to pluralised and both still remain // This is a transitional edge case if a string went from singular to pluralised and both still remain
// in the translation json file. Use the singular translation as `other` and merge pluralisation atop. // in the translation json file. Use the singular translation as `other` and merge pluralisation atop.
obj = outTrs[keyParts[0]] = { obj = outTrs[keyParts[0]] = {
"other": inTrs[key], other: inTrs[key],
}; };
console.warn("Found entry in i18n file in both singular and pluralised form", keyParts[0]); console.warn("Found entry in i18n file in both singular and pluralised form", keyParts[0]);
} }
@ -258,8 +253,8 @@ regenerate the file, adding its content-hashed filename to langFileMap
and regenerating languages.json with the new filename and regenerating languages.json with the new filename
*/ */
function watchLanguage(lang, dest, langFileMap) { function watchLanguage(lang, dest, langFileMap) {
const reactSdkFile = 'node_modules/matrix-react-sdk/src/i18n/strings/' + lang + '.json'; const reactSdkFile = "node_modules/matrix-react-sdk/src/i18n/strings/" + lang + ".json";
const riotWebFile = 'src/i18n/strings/' + lang + '.json'; const riotWebFile = "src/i18n/strings/" + lang + ".json";
// XXX: Use a debounce because for some reason if we read the language // XXX: Use a debounce because for some reason if we read the language
// file immediately after the FS event is received, the file contents // file immediately after the FS event is received, the file contents
@ -271,16 +266,13 @@ function watchLanguage(lang, dest, langFileMap) {
} }
makeLangDebouncer = setTimeout(() => { makeLangDebouncer = setTimeout(() => {
const filename = genLangFile(lang, dest); const filename = genLangFile(lang, dest);
langFileMap[lang]=filename; langFileMap[lang] = filename;
genLangList(langFileMap); genLangList(langFileMap);
}, 500); }, 500);
}; };
[reactSdkFile, riotWebFile].forEach(function(f) { [reactSdkFile, riotWebFile].forEach(function (f) {
chokidar.watch(f) chokidar.watch(f).on("add", makeLang).on("change", makeLang).on("error", errCheck);
.on('add', makeLang)
.on('change', makeLang)
.on('error', errCheck);
}); });
} }
@ -294,7 +286,7 @@ const I18N_FILENAME_MAP = INCLUDE_LANGS.reduce((m, l) => {
genLangList(I18N_FILENAME_MAP); genLangList(I18N_FILENAME_MAP);
if (watch) { if (watch) {
INCLUDE_LANGS.forEach(l => watchLanguage(l.value, I18N_DEST, I18N_FILENAME_MAP)); INCLUDE_LANGS.forEach((l) => watchLanguage(l.value, I18N_DEST, I18N_FILENAME_MAP));
} }
// non-language resources // non-language resources

View File

@ -19,20 +19,20 @@ import type { Renderer } from "react-dom";
import type { logger } from "matrix-js-sdk/src/logger"; import type { logger } from "matrix-js-sdk/src/logger";
type ElectronChannel = type ElectronChannel =
"app_onAction" | | "app_onAction"
"before-quit" | | "before-quit"
"check_updates" | | "check_updates"
"install_update" | | "install_update"
"ipcCall" | | "ipcCall"
"ipcReply" | | "ipcReply"
"loudNotification" | | "loudNotification"
"preferences" | | "preferences"
"seshat" | | "seshat"
"seshatReply" | | "seshatReply"
"setBadgeCount" | | "setBadgeCount"
"update-downloaded" | | "update-downloaded"
"userDownloadCompleted" | | "userDownloadCompleted"
"userDownloadAction"; | "userDownloadAction";
declare global { declare global {
interface Window { interface Window {

View File

@ -1,4 +1,4 @@
declare module '!!raw-loader!*' { declare module "!!raw-loader!*" {
const contents: string; const contents: string;
export default contents; export default contents;
} }

View File

@ -16,7 +16,7 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { _t } from "matrix-react-sdk/src/languageHandler"; import { _t } from "matrix-react-sdk/src/languageHandler";
import SdkConfig from 'matrix-react-sdk/src/SdkConfig'; import SdkConfig from "matrix-react-sdk/src/SdkConfig";
// directly import the style here as this layer does not support rethemedex at this time so no matrix-react-sdk // directly import the style here as this layer does not support rethemedex at this time so no matrix-react-sdk
// PostCSS variables will be accessible. // PostCSS variables will be accessible.
@ -32,115 +32,131 @@ const CompatibilityView: React.FC<IProps> = ({ onAccept }) => {
let ios = null; let ios = null;
const iosCustomUrl = mobileBuilds?.ios; const iosCustomUrl = mobileBuilds?.ios;
if (iosCustomUrl !== null) { // could be undefined or a string if (iosCustomUrl !== null) {
ios = <> // could be undefined or a string
<p><strong>iOS</strong> (iPhone or iPad)</p> ios = (
<a <>
href={iosCustomUrl || "https://apps.apple.com/app/vector/id1083446067"} <p>
target="_blank" <strong>iOS</strong> (iPhone or iPad)
className="mx_ClearDecoration" </p>
> <a
<img height="48" src="themes/element/img/download/apple.svg" alt="Apple App Store" /> href={iosCustomUrl || "https://apps.apple.com/app/vector/id1083446067"}
</a> target="_blank"
</>; className="mx_ClearDecoration"
>
<img height="48" src="themes/element/img/download/apple.svg" alt="Apple App Store" />
</a>
</>
);
} }
let android = [<p className="mx_Spacer" key="header"><strong>Android</strong></p>]; let android = [
<p className="mx_Spacer" key="header">
<strong>Android</strong>
</p>,
];
const andCustomUrl = mobileBuilds?.android; const andCustomUrl = mobileBuilds?.android;
const fdroidCustomUrl = mobileBuilds?.fdroid; const fdroidCustomUrl = mobileBuilds?.fdroid;
if (andCustomUrl !== null) { // undefined or string if (andCustomUrl !== null) {
android.push(<a // undefined or string
href={andCustomUrl || "https://play.google.com/store/apps/details?id=im.vector.app"} android.push(
target="_blank" <a
className="mx_ClearDecoration" href={andCustomUrl || "https://play.google.com/store/apps/details?id=im.vector.app"}
key="android" target="_blank"
> className="mx_ClearDecoration"
<img height="48" src="themes/element/img/download/google.svg" alt="Google Play Store" /> key="android"
</a>); >
<img height="48" src="themes/element/img/download/google.svg" alt="Google Play Store" />
</a>,
);
} }
if (fdroidCustomUrl !== null) { // undefined or string if (fdroidCustomUrl !== null) {
android.push(<a // undefined or string
href={fdroidCustomUrl || "https://f-droid.org/repository/browse/?fdid=im.vector.app"} android.push(
target="_blank" <a
className="mx_ClearDecoration" href={fdroidCustomUrl || "https://f-droid.org/repository/browse/?fdid=im.vector.app"}
key="fdroid" target="_blank"
> className="mx_ClearDecoration"
<img height="48" src="themes/element/img/download/fdroid.svg" alt="F-Droid" /> key="fdroid"
</a>); >
<img height="48" src="themes/element/img/download/fdroid.svg" alt="F-Droid" />
</a>,
);
} }
if (android.length === 1) { // just a header, meaning no links if (android.length === 1) {
// just a header, meaning no links
android = []; android = [];
} }
let mobileHeader = <h2 id="step2_heading">{ _t("Use %(brand)s on mobile", { brand }) }</h2>; let mobileHeader = <h2 id="step2_heading">{_t("Use %(brand)s on mobile", { brand })}</h2>;
if (!android.length && !ios) { if (!android.length && !ios) {
mobileHeader = null; mobileHeader = null;
} }
return <div className="mx_ErrorView"> return (
<div className="mx_ErrorView_container"> <div className="mx_ErrorView">
<div className="mx_HomePage_header"> <div className="mx_ErrorView_container">
<span className="mx_HomePage_logo"> <div className="mx_HomePage_header">
<img height="42" src="themes/element/img/logos/element-logo.svg" alt="Element" /> <span className="mx_HomePage_logo">
</span> <img height="42" src="themes/element/img/logos/element-logo.svg" alt="Element" />
<h1>{ _t("Unsupported browser") }</h1> </span>
</div> <h1>{_t("Unsupported browser")}</h1>
</div>
<div className="mx_HomePage_col"> <div className="mx_HomePage_col">
<div className="mx_HomePage_row"> <div className="mx_HomePage_row">
<div> <div>
<h2 id="step1_heading">{ _t("Your browser can't run %(brand)s", { brand }) }</h2> <h2 id="step1_heading">{_t("Your browser can't run %(brand)s", { brand })}</h2>
<p> <p>
{ _t( {_t(
"%(brand)s uses advanced browser features which aren't " + "%(brand)s uses advanced browser features which aren't " +
"supported by your current browser.", "supported by your current browser.",
{ brand }, { brand },
) } )}
</p> </p>
<p> <p>
{ _t( {_t(
'Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, ' + "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, " +
'or <safariLink>Safari</safariLink> for the best experience.', "or <safariLink>Safari</safariLink> for the best experience.",
{}, {},
{ {
'chromeLink': (sub) => <a href="https://www.google.com/chrome">{ sub }</a>, chromeLink: (sub) => <a href="https://www.google.com/chrome">{sub}</a>,
'firefoxLink': (sub) => <a href="https://firefox.com">{ sub }</a>, firefoxLink: (sub) => <a href="https://firefox.com">{sub}</a>,
'safariLink': (sub) => <a href="https://apple.com/safari">{ sub }</a>, safariLink: (sub) => <a href="https://apple.com/safari">{sub}</a>,
}, },
) } )}
</p> </p>
<p> <p>
{ _t( {_t(
"You can continue using your current browser, but some or all features may not work " + "You can continue using your current browser, but some or all features may not work " +
"and the look and feel of the application may be incorrect.", "and the look and feel of the application may be incorrect.",
) } )}
</p> </p>
<button onClick={onAccept}> <button onClick={onAccept}>{_t("I understand the risks and wish to continue")}</button>
{ _t("I understand the risks and wish to continue") } </div>
</button>
</div> </div>
</div> </div>
</div>
<div className="mx_HomePage_col"> <div className="mx_HomePage_col">
<div className="mx_HomePage_row"> <div className="mx_HomePage_row">
<div> <div>
{ mobileHeader } {mobileHeader}
{ ios } {ios}
{ android } {android}
</div>
</div> </div>
</div> </div>
</div>
<div className="mx_HomePage_row mx_Center mx_Spacer"> <div className="mx_HomePage_row mx_Center mx_Spacer">
<p className="mx_Spacer"> <p className="mx_Spacer">
<a href="https://element.io" target="_blank" className="mx_FooterLink"> <a href="https://element.io" target="_blank" className="mx_FooterLink">
{ _t("Go to element.io") } {_t("Go to element.io")}
</a> </a>
</p> </p>
</div>
</div> </div>
</div> </div>
</div>; );
}; };
export default CompatibilityView; export default CompatibilityView;

View File

@ -28,34 +28,33 @@ interface IProps {
} }
const ErrorView: React.FC<IProps> = ({ title, messages }) => { const ErrorView: React.FC<IProps> = ({ title, messages }) => {
return <div className="mx_ErrorView"> return (
<div className="mx_ErrorView_container"> <div className="mx_ErrorView">
<div className="mx_HomePage_header"> <div className="mx_ErrorView_container">
<span className="mx_HomePage_logo"> <div className="mx_HomePage_header">
<img height="42" src="themes/element/img/logos/element-logo.svg" alt="Element" /> <span className="mx_HomePage_logo">
</span> <img height="42" src="themes/element/img/logos/element-logo.svg" alt="Element" />
<h1>{ _t("Failed to start") }</h1> </span>
</div> <h1>{_t("Failed to start")}</h1>
<div className="mx_HomePage_col"> </div>
<div className="mx_HomePage_row"> <div className="mx_HomePage_col">
<div> <div className="mx_HomePage_row">
<h2 id="step1_heading">{ title }</h2> <div>
{ messages && messages.map(msg => <p key={msg}> <h2 id="step1_heading">{title}</h2>
{ msg } {messages && messages.map((msg) => <p key={msg}>{msg}</p>)}
</p>) } </div>
</div> </div>
</div> </div>
</div> <div className="mx_HomePage_row mx_Center mx_Spacer">
<div className="mx_HomePage_row mx_Center mx_Spacer"> <p className="mx_Spacer">
<p className="mx_Spacer"> <a href="https://element.io" target="_blank" className="mx_FooterLink">
<a href="https://element.io" target="_blank" className="mx_FooterLink"> {_t("Go to element.io")}
{ _t("Go to element.io") } </a>
</a> </p>
</p> </div>
</div> </div>
</div> </div>
</div>; );
}; };
export default ErrorView; export default ErrorView;

View File

@ -15,31 +15,33 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactElement } from 'react'; import React, { ReactElement } from "react";
import SdkConfig from 'matrix-react-sdk/src/SdkConfig'; import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import { _t } from 'matrix-react-sdk/src/languageHandler'; import { _t } from "matrix-react-sdk/src/languageHandler";
const VectorAuthFooter = (): ReactElement => { const VectorAuthFooter = (): ReactElement => {
const brandingConfig = SdkConfig.getObject("branding"); const brandingConfig = SdkConfig.getObject("branding");
const links = brandingConfig?.get("auth_footer_links") ?? [ const links = brandingConfig?.get("auth_footer_links") ?? [
{ "text": "Blog", "url": "https://element.io/blog" }, { text: "Blog", url: "https://element.io/blog" },
{ "text": "Twitter", "url": "https://twitter.com/element_hq" }, { text: "Twitter", url: "https://twitter.com/element_hq" },
{ "text": "GitHub", "url": "https://github.com/vector-im/element-web" }, { text: "GitHub", url: "https://github.com/vector-im/element-web" },
]; ];
const authFooterLinks = []; const authFooterLinks = [];
for (const linkEntry of links) { for (const linkEntry of links) {
authFooterLinks.push( authFooterLinks.push(
<a href={linkEntry.url} key={linkEntry.text} target="_blank" rel="noreferrer noopener"> <a href={linkEntry.url} key={linkEntry.text} target="_blank" rel="noreferrer noopener">
{ linkEntry.text } {linkEntry.text}
</a>, </a>,
); );
} }
return ( return (
<footer className="mx_AuthFooter" role="contentinfo"> <footer className="mx_AuthFooter" role="contentinfo">
{ authFooterLinks } {authFooterLinks}
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t('Powered by Matrix') }</a> <a href="https://matrix.org" target="_blank" rel="noreferrer noopener">
{_t("Powered by Matrix")}
</a>
</footer> </footer>
); );
}; };

View File

@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as React from 'react'; import * as React from "react";
import SdkConfig from 'matrix-react-sdk/src/SdkConfig'; import SdkConfig from "matrix-react-sdk/src/SdkConfig";
export default class VectorAuthHeaderLogo extends React.PureComponent { export default class VectorAuthHeaderLogo extends React.PureComponent {
public render(): React.ReactElement { public render(): React.ReactElement {

View File

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as React from 'react'; import * as React from "react";
import SdkConfig from 'matrix-react-sdk/src/SdkConfig'; import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import VectorAuthFooter from "./VectorAuthFooter"; import VectorAuthFooter from "./VectorAuthFooter";
@ -48,25 +48,25 @@ export default class VectorAuthPage extends React.PureComponent {
}; };
const modalStyle: React.CSSProperties = { const modalStyle: React.CSSProperties = {
position: 'relative', position: "relative",
background: 'initial', background: "initial",
}; };
const blurStyle: React.CSSProperties = { const blurStyle: React.CSSProperties = {
position: 'absolute', position: "absolute",
top: 0, top: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
left: 0, left: 0,
filter: 'blur(40px)', filter: "blur(40px)",
background: pageStyle.background, background: pageStyle.background,
}; };
const modalContentStyle: React.CSSProperties = { const modalContentStyle: React.CSSProperties = {
display: 'flex', display: "flex",
zIndex: 1, zIndex: 1,
background: 'rgba(255, 255, 255, 0.59)', background: "rgba(255, 255, 255, 0.59)",
borderRadius: '8px', borderRadius: "8px",
}; };
return ( return (
@ -74,7 +74,7 @@ export default class VectorAuthPage extends React.PureComponent {
<div className="mx_AuthPage_modal" style={modalStyle}> <div className="mx_AuthPage_modal" style={modalStyle}>
<div className="mx_AuthPage_modalBlur" style={blurStyle} /> <div className="mx_AuthPage_modalBlur" style={blurStyle} />
<div className="mx_AuthPage_modalContent" style={modalContentStyle}> <div className="mx_AuthPage_modalContent" style={modalContentStyle}>
{ this.props.children } {this.props.children}
</div> </div>
</div> </div>
<VectorAuthFooter /> <VectorAuthFooter />

View File

@ -70,8 +70,8 @@ export default class Favicon {
this.baseImage.setAttribute("crossOrigin", "anonymous"); this.baseImage.setAttribute("crossOrigin", "anonymous");
this.baseImage.onload = (): void => { this.baseImage.onload = (): void => {
// get height and width of the favicon // get height and width of the favicon
this.canvas.height = (this.baseImage.height > 0) ? this.baseImage.height : 32; this.canvas.height = this.baseImage.height > 0 ? this.baseImage.height : 32;
this.canvas.width = (this.baseImage.width > 0) ? this.baseImage.width : 32; this.canvas.width = this.baseImage.width > 0 ? this.baseImage.width : 32;
this.context = this.canvas.getContext("2d"); this.context = this.canvas.getContext("2d");
this.ready(); this.ready();
}; };
@ -89,7 +89,10 @@ export default class Favicon {
this.context.drawImage(this.baseImage, 0, 0, this.canvas.width, this.canvas.height); this.context.drawImage(this.baseImage, 0, 0, this.canvas.width, this.canvas.height);
} }
private options(n: number | string, params: IParams): { private options(
n: number | string,
params: IParams,
): {
n: string | number; n: string | number;
len: number; len: number;
x: number; x: number;
@ -98,7 +101,7 @@ export default class Favicon {
h: number; h: number;
} { } {
const opt = { const opt = {
n: ((typeof n) === "number") ? Math.abs(n as number | 0) : n, n: typeof n === "number" ? Math.abs(n as number | 0) : n,
len: ("" + n).length, len: ("" + n).length,
// badge positioning constants as percentages // badge positioning constants as percentages
x: 0.4, x: 0.4,
@ -174,8 +177,8 @@ export default class Favicon {
this.context.stroke(); this.context.stroke();
this.context.fillStyle = params.textColor; this.context.fillStyle = params.textColor;
if ((typeof opt.n) === "number" && opt.n > 999) { if (typeof opt.n === "number" && opt.n > 999) {
const count = ((opt.n > 9999) ? 9 : Math.floor(opt.n as number / 1000)) + "k+"; const count = (opt.n > 9999 ? 9 : Math.floor((opt.n as number) / 1000)) + "k+";
this.context.fillText(count, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2)); this.context.fillText(count, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2));
} else { } else {
this.context.fillText("" + opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); this.context.fillText("" + opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));
@ -209,7 +212,7 @@ export default class Favicon {
newIcon.setAttribute("href", url); newIcon.setAttribute("href", url);
old.parentNode?.removeChild(old); old.parentNode?.removeChild(old);
} else { } else {
this.icons.forEach(icon => { this.icons.forEach((icon) => {
icon.setAttribute("href", url); icon.setAttribute("href", url);
}); });
} }
@ -236,7 +239,7 @@ export default class Favicon {
const icons: HTMLLinkElement[] = []; const icons: HTMLLinkElement[] = [];
const links = window.document.getElementsByTagName("head")[0].getElementsByTagName("link"); const links = window.document.getElementsByTagName("head")[0].getElementsByTagName("link");
for (const link of links) { for (const link of links) {
if ((/(^|\s)icon(\s|$)/i).test(link.getAttribute("rel"))) { if (/(^|\s)icon(\s|$)/i.test(link.getAttribute("rel"))) {
icons.push(link); icons.push(link);
} }
} }
@ -252,7 +255,7 @@ export default class Favicon {
window.document.getElementsByTagName("head")[0].appendChild(elms[0]); window.document.getElementsByTagName("head")[0].appendChild(elms[0]);
} }
elms.forEach(item => { elms.forEach((item) => {
item.setAttribute("type", "image/png"); item.setAttribute("type", "image/png");
}); });
return elms; return elms;

View File

@ -21,10 +21,10 @@ limitations under the License.
// To ensure we load the browser-matrix version first // To ensure we load the browser-matrix version first
import "matrix-js-sdk/src/browser-index"; import "matrix-js-sdk/src/browser-index";
import React, { ReactElement } from 'react'; import React, { ReactElement } from "react";
import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg'; import PlatformPeg from "matrix-react-sdk/src/PlatformPeg";
import { _td, newTranslatableError } from 'matrix-react-sdk/src/languageHandler'; import { _td, newTranslatableError } from "matrix-react-sdk/src/languageHandler";
import AutoDiscoveryUtils from 'matrix-react-sdk/src/utils/AutoDiscoveryUtils'; import AutoDiscoveryUtils from "matrix-react-sdk/src/utils/AutoDiscoveryUtils";
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery"; import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
import * as Lifecycle from "matrix-react-sdk/src/Lifecycle"; import * as Lifecycle from "matrix-react-sdk/src/Lifecycle";
import SdkConfig, { parseSsoRedirectOptions } from "matrix-react-sdk/src/SdkConfig"; import SdkConfig, { parseSsoRedirectOptions } from "matrix-react-sdk/src/SdkConfig";
@ -34,7 +34,7 @@ import { createClient } from "matrix-js-sdk/src/matrix";
import { SnakedObject } from "matrix-react-sdk/src/utils/SnakedObject"; import { SnakedObject } from "matrix-react-sdk/src/utils/SnakedObject";
import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat"; import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat";
import { parseQs } from './url_utils'; import { parseQs } from "./url_utils";
import VectorBasePlatform from "./platform/VectorBasePlatform"; import VectorBasePlatform from "./platform/VectorBasePlatform";
import { getScreenFromLocation, init as initRouting, onNewScreen } from "./routing"; import { getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
@ -58,25 +58,20 @@ window.matrixLogger = logger;
function makeRegistrationUrl(params: object): string { function makeRegistrationUrl(params: object): string {
let url; let url;
if (window.location.protocol === "vector:") { if (window.location.protocol === "vector:") {
url = 'https://app.element.io/#/register'; url = "https://app.element.io/#/register";
} else { } else {
url = ( url = window.location.protocol + "//" + window.location.host + window.location.pathname + "#/register";
window.location.protocol + '//' +
window.location.host +
window.location.pathname +
'#/register'
);
} }
const keys = Object.keys(params); const keys = Object.keys(params);
for (let i = 0; i < keys.length; ++i) { for (let i = 0; i < keys.length; ++i) {
if (i === 0) { if (i === 0) {
url += '?'; url += "?";
} else { } else {
url += '&'; url += "&";
} }
const k = keys[i]; const k = keys[i];
url += k + '=' + encodeURIComponent(params[k]); url += k + "=" + encodeURIComponent(params[k]);
} }
return url; return url;
} }
@ -99,7 +94,7 @@ export async function loadApp(fragParams: {}): Promise<ReactElement> {
const params = parseQs(window.location); const params = parseQs(window.location);
const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname; const urlWithoutQuery = window.location.protocol + "//" + window.location.host + window.location.pathname;
logger.log("Vector starting at " + urlWithoutQuery); logger.log("Vector starting at " + urlWithoutQuery);
(platform as VectorBasePlatform).startUpdater(); (platform as VectorBasePlatform).startUpdater();
@ -115,7 +110,7 @@ export async function loadApp(fragParams: {}): Promise<ReactElement> {
const ssoRedirects = parseSsoRedirectOptions(config); const ssoRedirects = parseSsoRedirectOptions(config);
let autoRedirect = ssoRedirects.immediate === true; let autoRedirect = ssoRedirects.immediate === true;
// XXX: This path matching is a bit brittle, but better to do it early instead of in the app code. // XXX: This path matching is a bit brittle, but better to do it early instead of in the app code.
const isWelcomeOrLanding = window.location.hash === '#/welcome' || window.location.hash === '#'; const isWelcomeOrLanding = window.location.hash === "#/welcome" || window.location.hash === "#";
if (!autoRedirect && ssoRedirects.on_welcome_page && isWelcomeOrLanding) { if (!autoRedirect && ssoRedirects.on_welcome_page && isWelcomeOrLanding) {
autoRedirect = true; autoRedirect = true;
} }
@ -133,20 +128,21 @@ export async function loadApp(fragParams: {}): Promise<ReactElement> {
return; return;
} }
const defaultDeviceName = snakedConfig.get("default_device_display_name") const defaultDeviceName = snakedConfig.get("default_device_display_name") ?? platform.getDefaultDeviceDisplayName();
?? platform.getDefaultDeviceDisplayName();
return <MatrixChat return (
onNewScreen={onNewScreen} <MatrixChat
makeRegistrationUrl={makeRegistrationUrl} onNewScreen={onNewScreen}
config={config} makeRegistrationUrl={makeRegistrationUrl}
realQueryParams={params} config={config}
startingFragmentQueryParams={fragParams} realQueryParams={params}
enableGuest={!config.disable_guests} startingFragmentQueryParams={fragParams}
onTokenLoginCompleted={onTokenLoginCompleted} enableGuest={!config.disable_guests}
initialScreenAfterLogin={getScreenFromLocation(window.location)} onTokenLoginCompleted={onTokenLoginCompleted}
defaultDeviceDisplayName={defaultDeviceName} initialScreenAfterLogin={getScreenFromLocation(window.location)}
/>; defaultDeviceDisplayName={defaultDeviceName}
/>
);
} }
async function verifyServerConfig(): Promise<IConfigOptions> { async function verifyServerConfig(): Promise<IConfigOptions> {
@ -164,18 +160,20 @@ async function verifyServerConfig(): Promise<IConfigOptions> {
// validators for that purpose. // validators for that purpose.
const config = SdkConfig.get(); const config = SdkConfig.get();
let wkConfig = config['default_server_config']; // overwritten later under some conditions let wkConfig = config["default_server_config"]; // overwritten later under some conditions
const serverName = config['default_server_name']; const serverName = config["default_server_name"];
const hsUrl = config['default_hs_url']; const hsUrl = config["default_hs_url"];
const isUrl = config['default_is_url']; const isUrl = config["default_is_url"];
const incompatibleOptions = [wkConfig, serverName, hsUrl].filter(i => !!i); const incompatibleOptions = [wkConfig, serverName, hsUrl].filter((i) => !!i);
if (incompatibleOptions.length > 1) { if (incompatibleOptions.length > 1) {
// noinspection ExceptionCaughtLocallyJS // noinspection ExceptionCaughtLocallyJS
throw newTranslatableError(_td( throw newTranslatableError(
"Invalid configuration: can only specify one of default_server_config, default_server_name, " + _td(
"or default_hs_url.", "Invalid configuration: can only specify one of default_server_config, default_server_name, " +
)); "or default_hs_url.",
),
);
} }
if (incompatibleOptions.length < 1) { if (incompatibleOptions.length < 1) {
// noinspection ExceptionCaughtLocallyJS // noinspection ExceptionCaughtLocallyJS
@ -186,17 +184,17 @@ async function verifyServerConfig(): Promise<IConfigOptions> {
logger.log("Config uses a default_hs_url - constructing a default_server_config using this information"); logger.log("Config uses a default_hs_url - constructing a default_server_config using this information");
logger.warn( logger.warn(
"DEPRECATED CONFIG OPTION: In the future, default_hs_url will not be accepted. Please use " + "DEPRECATED CONFIG OPTION: In the future, default_hs_url will not be accepted. Please use " +
"default_server_config instead.", "default_server_config instead.",
); );
wkConfig = { wkConfig = {
"m.homeserver": { "m.homeserver": {
"base_url": hsUrl, base_url: hsUrl,
}, },
}; };
if (isUrl) { if (isUrl) {
wkConfig["m.identity_server"] = { wkConfig["m.identity_server"] = {
"base_url": isUrl, base_url: isUrl,
}; };
} }
} }
@ -211,7 +209,7 @@ async function verifyServerConfig(): Promise<IConfigOptions> {
logger.log("Config uses a default_server_name - doing .well-known lookup"); logger.log("Config uses a default_server_name - doing .well-known lookup");
logger.warn( logger.warn(
"DEPRECATED CONFIG OPTION: In the future, default_server_name will not be accepted. Please " + "DEPRECATED CONFIG OPTION: In the future, default_server_name will not be accepted. Please " +
"use default_server_config instead.", "use default_server_config instead.",
); );
discoveryResult = await AutoDiscovery.findClientConfig(serverName); discoveryResult = await AutoDiscovery.findClientConfig(serverName);
} }
@ -238,7 +236,7 @@ async function verifyServerConfig(): Promise<IConfigOptions> {
// Add the newly built config to the actual config for use by the app // Add the newly built config to the actual config for use by the app
logger.log("Updating SdkConfig with validated discovery information"); logger.log("Updating SdkConfig with validated discovery information");
SdkConfig.add({ "validated_server_config": validatedConfig }); SdkConfig.add({ validated_server_config: validatedConfig });
return SdkConfig.get(); return SdkConfig.get();
} }

View File

@ -28,7 +28,7 @@ limitations under the License.
* *
* For more details, see webpack.config.js:184 (string-replace-loader) * For more details, see webpack.config.js:184 (string-replace-loader)
*/ */
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === "development") {
("use theming"); ("use theming");
/** /**
* Clean up old hot-module script injections as they hog up memory * Clean up old hot-module script injections as they hog up memory
@ -40,8 +40,7 @@ if (process.env.NODE_ENV === 'development') {
const elements = Array.from(document.querySelectorAll("script[src*=hot-update]")); const elements = Array.from(document.querySelectorAll("script[src*=hot-update]"));
if (elements.length > 1) { if (elements.length > 1) {
const oldInjects = elements.slice(0, elements.length - 1); const oldInjects = elements.slice(0, elements.length - 1);
oldInjects.forEach(e => e.remove()); oldInjects.forEach((e) => e.remove());
} }
}, 1000); }, 1000);
} }

View File

@ -18,8 +18,8 @@ import type { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
// Load the config file. First try to load up a domain-specific config of the // Load the config file. First try to load up a domain-specific config of the
// form "config.$domain.json" and if that fails, fall back to config.json. // form "config.$domain.json" and if that fails, fall back to config.json.
export async function getVectorConfig(relativeLocation=''): Promise<IConfigOptions> { export async function getVectorConfig(relativeLocation = ""): Promise<IConfigOptions> {
if (relativeLocation !== '' && !relativeLocation.endsWith('/')) relativeLocation += '/'; if (relativeLocation !== "" && !relativeLocation.endsWith("/")) relativeLocation += "/";
const specificConfigPromise = getConfig(`${relativeLocation}config.${document.domain}.json`); const specificConfigPromise = getConfig(`${relativeLocation}config.${document.domain}.json`);
const generalConfigPromise = getConfig(relativeLocation + "config.json"); const generalConfigPromise = getConfig(relativeLocation + "config.json");

View File

@ -22,13 +22,13 @@ import { logger } from "matrix-js-sdk/src/logger";
// These are things that can run before the skin loads - be careful not to reference the react-sdk though. // These are things that can run before the skin loads - be careful not to reference the react-sdk though.
import { parseQsFromFragment } from "./url_utils"; import { parseQsFromFragment } from "./url_utils";
import './modernizr'; import "./modernizr";
// Require common CSS here; this will make webpack process it into bundle.css. // Require common CSS here; this will make webpack process it into bundle.css.
// Our own CSS (which is themed) is imported via separate webpack entry points // Our own CSS (which is themed) is imported via separate webpack entry points
// in webpack.config.js // in webpack.config.js
require('gfm.css/gfm.css'); require("gfm.css/gfm.css");
require('katex/dist/katex.css'); require("katex/dist/katex.css");
/** /**
* This require is necessary only for purposes of CSS hot-reload, as otherwise * This require is necessary only for purposes of CSS hot-reload, as otherwise
@ -37,8 +37,8 @@ require('katex/dist/katex.css');
* *
* On production build it's going to be an empty module, so don't worry about that. * On production build it's going to be an empty module, so don't worry about that.
*/ */
require('./devcss'); require("./devcss");
require('./localstorage-fix'); require("./localstorage-fix");
async function settled(...promises: Array<Promise<any>>): Promise<void> { async function settled(...promises: Array<Promise<any>>): Promise<void> {
for (const prom of promises) { for (const prom of promises) {
@ -60,19 +60,16 @@ function checkBrowserFeatures(): boolean {
// in it for some features we depend on. // in it for some features we depend on.
// Modernizr requires rules to be lowercase with no punctuation. // Modernizr requires rules to be lowercase with no punctuation.
// ES2018: http://262.ecma-international.org/9.0/#sec-promise.prototype.finally // ES2018: http://262.ecma-international.org/9.0/#sec-promise.prototype.finally
window.Modernizr.addTest("promiseprototypefinally", () => window.Modernizr.addTest("promiseprototypefinally", () => typeof window.Promise?.prototype?.finally === "function");
typeof window.Promise?.prototype?.finally === "function");
// ES2020: http://262.ecma-international.org/#sec-promise.allsettled // ES2020: http://262.ecma-international.org/#sec-promise.allsettled
window.Modernizr.addTest("promiseallsettled", () => window.Modernizr.addTest("promiseallsettled", () => typeof window.Promise?.allSettled === "function");
typeof window.Promise?.allSettled === "function");
// ES2018: https://262.ecma-international.org/9.0/#sec-get-regexp.prototype.dotAll // ES2018: https://262.ecma-international.org/9.0/#sec-get-regexp.prototype.dotAll
window.Modernizr.addTest("regexpdotall", () => ( window.Modernizr.addTest(
window.RegExp?.prototype && "regexpdotall",
!!Object.getOwnPropertyDescriptor(window.RegExp.prototype, "dotAll")?.get () => window.RegExp?.prototype && !!Object.getOwnPropertyDescriptor(window.RegExp.prototype, "dotAll")?.get,
)); );
// ES2019: http://262.ecma-international.org/10.0/#sec-object.fromentries // ES2019: http://262.ecma-international.org/10.0/#sec-object.fromentries
window.Modernizr.addTest("objectfromentries", () => window.Modernizr.addTest("objectfromentries", () => typeof window.Object?.fromEntries === "function");
typeof window.Object?.fromEntries === "function");
const featureList = Object.keys(window.Modernizr); const featureList = Object.keys(window.Modernizr);
@ -80,8 +77,8 @@ function checkBrowserFeatures(): boolean {
for (const feature of featureList) { for (const feature of featureList) {
if (window.Modernizr[feature] === undefined) { if (window.Modernizr[feature] === undefined) {
logger.error( logger.error(
"Looked for feature '%s' but Modernizr has no results for this. " + "Looked for feature '%s' but Modernizr has no results for this. " + "Has it been configured correctly?",
"Has it been configured correctly?", feature, feature,
); );
return false; return false;
} }
@ -120,7 +117,8 @@ async function start(): Promise<void> {
} = await import( } = await import(
/* webpackChunkName: "init" */ /* webpackChunkName: "init" */
/* webpackPreload: true */ /* webpackPreload: true */
"./init"); "./init"
);
try { try {
// give rageshake a chance to load/fail, we don't actually assert rageshake loads, we allow it to fail if no IDB // give rageshake a chance to load/fail, we don't actually assert rageshake loads, we allow it to fail if no IDB
@ -178,12 +176,12 @@ async function start(): Promise<void> {
// error handling begins here // error handling begins here
// ########################## // ##########################
if (!acceptBrowser) { if (!acceptBrowser) {
await new Promise<void>(resolve => { await new Promise<void>((resolve) => {
logger.error("Browser is missing required features."); logger.error("Browser is missing required features.");
// take to a different landing page to AWOOOOOGA at the user // take to a different landing page to AWOOOOOGA at the user
showIncompatibleBrowser(() => { showIncompatibleBrowser(() => {
if (window.localStorage) { if (window.localStorage) {
window.localStorage.setItem('mx_accepts_unsupported_browser', String(true)); window.localStorage.setItem("mx_accepts_unsupported_browser", String(true));
} }
logger.log("User accepts the compatibility risks."); logger.log("User accepts the compatibility risks.");
resolve(); resolve();
@ -199,12 +197,13 @@ async function start(): Promise<void> {
if (error.err && error.err instanceof SyntaxError) { if (error.err && error.err instanceof SyntaxError) {
// This uses the default brand since the app config is unavailable. // This uses the default brand since the app config is unavailable.
return showError(_t("Your Element is misconfigured"), [ return showError(_t("Your Element is misconfigured"), [
_t("Your Element configuration contains invalid JSON. " +
"Please correct the problem and reload the page."),
_t( _t(
"The message from the parser is: %(message)s", "Your Element configuration contains invalid JSON. " +
{ message: error.err.message || _t("Invalid JSON") }, "Please correct the problem and reload the page.",
), ),
_t("The message from the parser is: %(message)s", {
message: error.err.message || _t("Invalid JSON"),
}),
]); ]);
} }
return showError(_t("Unable to load config file: please refresh the page to try again.")); return showError(_t("Unable to load config file: please refresh the page to try again."));
@ -237,7 +236,7 @@ async function start(): Promise<void> {
} }
} }
start().catch(err => { start().catch((err) => {
logger.error(err); logger.error(err);
// show the static error in an iframe to not lose any context / console data // show the static error in an iframe to not lose any context / console data
// with some basic styling to make the iframe full page // with some basic styling to make the iframe full page

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { IndexedDBStoreWorker } from 'matrix-js-sdk/src/indexeddb-worker'; import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker";
const remoteWorker = new IndexedDBStoreWorker(postMessage as InstanceType<typeof Worker>["postMessage"]); const remoteWorker = new IndexedDBStoreWorker(postMessage as InstanceType<typeof Worker>["postMessage"]);

View File

@ -20,7 +20,7 @@ limitations under the License.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import olmWasmPath from "@matrix-org/olm/olm.wasm"; import olmWasmPath from "@matrix-org/olm/olm.wasm";
import Olm from '@matrix-org/olm'; import Olm from "@matrix-org/olm";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import * as React from "react"; import * as React from "react";
import * as languageHandler from "matrix-react-sdk/src/languageHandler"; import * as languageHandler from "matrix-react-sdk/src/languageHandler";
@ -45,7 +45,7 @@ export function preparePlatform(): void {
if (window.electron) { if (window.electron) {
logger.log("Using Electron platform"); logger.log("Using Electron platform");
PlatformPeg.set(new ElectronPlatform()); PlatformPeg.set(new ElectronPlatform());
} else if (window.matchMedia('(display-mode: standalone)').matches) { } else if (window.matchMedia("(display-mode: standalone)").matches) {
logger.log("Using PWA platform"); logger.log("Using PWA platform");
PlatformPeg.set(new PWAPlatform()); PlatformPeg.set(new PWAPlatform());
} else { } else {
@ -90,30 +90,35 @@ export function loadOlm(): Promise<void> {
*/ */
return Olm.init({ return Olm.init({
locateFile: () => olmWasmPath, locateFile: () => olmWasmPath,
}).then(() => { })
logger.log("Using WebAssembly Olm"); .then(() => {
}).catch((wasmLoadError) => { logger.log("Using WebAssembly Olm");
logger.log("Failed to load Olm: trying legacy version", wasmLoadError); })
return new Promise((resolve, reject) => { .catch((wasmLoadError) => {
const s = document.createElement('script'); logger.log("Failed to load Olm: trying legacy version", wasmLoadError);
s.src = 'olm_legacy.js'; // XXX: This should be cache-busted too return new Promise((resolve, reject) => {
s.onload = resolve; const s = document.createElement("script");
s.onerror = reject; s.src = "olm_legacy.js"; // XXX: This should be cache-busted too
document.body.appendChild(s); s.onload = resolve;
}).then(() => { s.onerror = reject;
// Init window.Olm, ie. the one just loaded by the script tag, document.body.appendChild(s);
// not 'Olm' which is still the failed wasm version. })
return window.Olm.init(); .then(() => {
}).then(() => { // Init window.Olm, ie. the one just loaded by the script tag,
logger.log("Using legacy Olm"); // not 'Olm' which is still the failed wasm version.
}).catch((legacyLoadError) => { return window.Olm.init();
logger.log("Both WebAssembly and asm.js Olm failed!", legacyLoadError); })
.then(() => {
logger.log("Using legacy Olm");
})
.catch((legacyLoadError) => {
logger.log("Both WebAssembly and asm.js Olm failed!", legacyLoadError);
});
}); });
});
} }
export async function loadLanguage(): Promise<void> { export async function loadLanguage(): Promise<void> {
const prefLang = SettingsStore.getValue("language", null, /*excludeDefault=*/true); const prefLang = SettingsStore.getValue("language", null, /*excludeDefault=*/ true);
let langs = []; let langs = [];
if (!prefLang) { if (!prefLang) {
@ -140,25 +145,35 @@ export async function loadApp(fragParams: {}): Promise<void> {
const module = await import( const module = await import(
/* webpackChunkName: "element-web-app" */ /* webpackChunkName: "element-web-app" */
/* webpackPreload: true */ /* webpackPreload: true */
"./app"); "./app"
window.matrixChat = ReactDOM.render(await module.loadApp(fragParams), );
document.getElementById('matrixchat')); window.matrixChat = ReactDOM.render(await module.loadApp(fragParams), document.getElementById("matrixchat"));
} }
export async function showError(title: string, messages?: string[]): Promise<void> { export async function showError(title: string, messages?: string[]): Promise<void> {
const ErrorView = (await import( const ErrorView = (
/* webpackChunkName: "error-view" */ await import(
"../async-components/structures/ErrorView")).default; /* webpackChunkName: "error-view" */
window.matrixChat = ReactDOM.render(<ErrorView title={title} messages={messages} />, "../async-components/structures/ErrorView"
document.getElementById('matrixchat')); )
).default;
window.matrixChat = ReactDOM.render(
<ErrorView title={title} messages={messages} />,
document.getElementById("matrixchat"),
);
} }
export async function showIncompatibleBrowser(onAccept): Promise<void> { export async function showIncompatibleBrowser(onAccept): Promise<void> {
const CompatibilityView = (await import( const CompatibilityView = (
/* webpackChunkName: "compatibility-view" */ await import(
"../async-components/structures/CompatibilityView")).default; /* webpackChunkName: "compatibility-view" */
window.matrixChat = ReactDOM.render(<CompatibilityView onAccept={onAccept} />, "../async-components/structures/CompatibilityView"
document.getElementById('matrixchat')); )
).default;
window.matrixChat = ReactDOM.render(
<CompatibilityView onAccept={onAccept} />,
document.getElementById("matrixchat"),
);
} }
export async function loadModules(): Promise<void> { export async function loadModules(): Promise<void> {

View File

@ -1,24 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>Jitsi Widget</title> <title>Jitsi Widget</title>
</head> </head>
<body> <body>
<div id="jitsiContainer"><!-- the js will put the conference here --></div> <div id="jitsiContainer"><!-- the js will put the conference here --></div>
<div id="joinButtonContainer"> <div id="joinButtonContainer">
<div class="joinConferenceFloating"> <div class="joinConferenceFloating">
<div class="joinConferencePrompt"> <div class="joinConferencePrompt">
<span class="icon"><!-- managed by CSS --></span> <span class="icon"><!-- managed by CSS --></span>
<!-- TODO: i18n --> <!-- TODO: i18n -->
<h2>Jitsi Video Conference</h2> <h2>Jitsi Video Conference</h2>
<div id="widgetActionContainer"> <div id="widgetActionContainer">
<button type="button" id="joinButton">Join Conference</button> <button type="button" id="joinButton">Join Conference</button>
</div>
</div>
</div> </div>
</div> </div>
</div> <!-- This script is not webpacked, and the script is downloaded at build time -->
</div> <script src="./jitsi_external_api.min.js"></script>
<!-- This script is not webpacked, and the script is downloaded at build time --> </body>
<script src="./jitsi_external_api.min.js"></script>
</body>
</html> </html>

View File

@ -17,10 +17,10 @@ limitations under the License.
/* TODO: Match the user's theme: https://github.com/vector-im/element-web/issues/12794 */ /* TODO: Match the user's theme: https://github.com/vector-im/element-web/issues/12794 */
@font-face { @font-face {
font-family: 'Nunito'; font-family: "Nunito";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('~matrix-react-sdk/res/fonts/Nunito/Nunito-Regular.ttf') format('truetype'); src: url("~matrix-react-sdk/res/fonts/Nunito/Nunito-Regular.ttf") format("truetype");
} }
$dark-fg: #edf3ff; $dark-fg: #edf3ff;
@ -38,7 +38,8 @@ body.theme-light {
color: $light-fg; color: $light-fg;
} }
body, html { body,
html {
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
@ -92,7 +93,7 @@ body, html {
margin-top: -$icon-size; /* to visually center the form */ margin-top: -$icon-size; /* to visually center the form */
&::before { &::before {
content: ''; content: "";
background-size: contain; background-size: contain;
background-color: $dark-fg; background-color: $dark-fg;
mask-repeat: no-repeat; mask-repeat: no-repeat;

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { KJUR } from 'jsrsasign'; import { KJUR } from "jsrsasign";
import { import {
IOpenIDCredentials, IOpenIDCredentials,
IWidgetApiRequest, IWidgetApiRequest,
@ -34,7 +34,7 @@ import { getVectorConfig } from "../getconfig";
// We have to trick webpack into loading our CSS for us. // We have to trick webpack into loading our CSS for us.
require("./index.pcss"); require("./index.pcss");
const JITSI_OPENIDTOKEN_JWT_AUTH = 'openidtoken-jwt'; const JITSI_OPENIDTOKEN_JWT_AUTH = "openidtoken-jwt";
// Dev note: we use raw JS without many dependencies to reduce bundle size. // Dev note: we use raw JS without many dependencies to reduce bundle size.
// We do not need all of React to render a Jitsi conference. // We do not need all of React to render a Jitsi conference.
@ -82,9 +82,9 @@ const setupCompleted = (async (): Promise<string | void> => {
// If we have these params, expect a widget API to be available (ie. to be in an iframe // If we have these params, expect a widget API to be available (ie. to be in an iframe
// inside a matrix client). Otherwise, assume we're on our own, eg. have been popped // inside a matrix client). Otherwise, assume we're on our own, eg. have been popped
// out into a browser. // out into a browser.
const parentUrl = qsParam('parentUrl', true); const parentUrl = qsParam("parentUrl", true);
const widgetId = qsParam('widgetId', true); const widgetId = qsParam("widgetId", true);
const theme = qsParam('theme', true); const theme = qsParam("theme", true);
if (theme) { if (theme) {
document.body.classList.add(`theme-${theme.replace(" ", "_")}`); document.body.classList.add(`theme-${theme.replace(" ", "_")}`);
@ -93,10 +93,10 @@ const setupCompleted = (async (): Promise<string | void> => {
// Set this up as early as possible because Element will be hitting it almost immediately. // Set this up as early as possible because Element will be hitting it almost immediately.
let widgetApiReady: Promise<void>; let widgetApiReady: Promise<void>;
if (parentUrl && widgetId) { if (parentUrl && widgetId) {
const parentOrigin = new URL(qsParam('parentUrl')).origin; const parentOrigin = new URL(qsParam("parentUrl")).origin;
widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin); widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin);
widgetApiReady = new Promise<void>(resolve => widgetApi.once("ready", resolve)); widgetApiReady = new Promise<void>((resolve) => widgetApi.once("ready", resolve));
widgetApi.requestCapabilities(VideoConferenceCapabilities); widgetApi.requestCapabilities(VideoConferenceCapabilities);
widgetApi.start(); widgetApi.start();
@ -134,39 +134,39 @@ const setupCompleted = (async (): Promise<string | void> => {
meetApi = null; meetApi = null;
closeConference(); closeConference();
} else { } else {
meetApi?.executeCommand('hangup'); meetApi?.executeCommand("hangup");
} }
}); });
handleAction(ElementWidgetActions.MuteAudio, async () => { handleAction(ElementWidgetActions.MuteAudio, async () => {
if (meetApi && !await meetApi.isAudioMuted()) { if (meetApi && !(await meetApi.isAudioMuted())) {
meetApi.executeCommand('toggleAudio'); meetApi.executeCommand("toggleAudio");
} }
}); });
handleAction(ElementWidgetActions.UnmuteAudio, async () => { handleAction(ElementWidgetActions.UnmuteAudio, async () => {
if (meetApi && await meetApi.isAudioMuted()) { if (meetApi && (await meetApi.isAudioMuted())) {
meetApi.executeCommand('toggleAudio'); meetApi.executeCommand("toggleAudio");
} }
}); });
handleAction(ElementWidgetActions.MuteVideo, async () => { handleAction(ElementWidgetActions.MuteVideo, async () => {
if (meetApi && !await meetApi.isVideoMuted()) { if (meetApi && !(await meetApi.isVideoMuted())) {
meetApi.executeCommand('toggleVideo'); meetApi.executeCommand("toggleVideo");
} }
}); });
handleAction(ElementWidgetActions.UnmuteVideo, async () => { handleAction(ElementWidgetActions.UnmuteVideo, async () => {
if (meetApi && await meetApi.isVideoMuted()) { if (meetApi && (await meetApi.isVideoMuted())) {
meetApi.executeCommand('toggleVideo'); meetApi.executeCommand("toggleVideo");
} }
}); });
handleAction(ElementWidgetActions.TileLayout, async () => { handleAction(ElementWidgetActions.TileLayout, async () => {
meetApi?.executeCommand('setTileView', true); meetApi?.executeCommand("setTileView", true);
}); });
handleAction(ElementWidgetActions.SpotlightLayout, async () => { handleAction(ElementWidgetActions.SpotlightLayout, async () => {
meetApi?.executeCommand('setTileView', false); meetApi?.executeCommand("setTileView", false);
}); });
handleAction(ElementWidgetActions.StartLiveStream, async ({ rtmpStreamKey }) => { handleAction(ElementWidgetActions.StartLiveStream, async ({ rtmpStreamKey }) => {
if (!meetApi) throw new Error("Conference not joined"); if (!meetApi) throw new Error("Conference not joined");
meetApi.executeCommand('startRecording', { meetApi.executeCommand("startRecording", {
mode: 'stream', mode: "stream",
// this looks like it should be rtmpStreamKey but we may be on too old // this looks like it should be rtmpStreamKey but we may be on too old
// a version of jitsi meet // a version of jitsi meet
//rtmpStreamKey, //rtmpStreamKey,
@ -178,23 +178,23 @@ const setupCompleted = (async (): Promise<string | void> => {
} }
// Populate the Jitsi params now // Populate the Jitsi params now
jitsiDomain = qsParam('conferenceDomain'); jitsiDomain = qsParam("conferenceDomain");
conferenceId = qsParam('conferenceId'); conferenceId = qsParam("conferenceId");
displayName = qsParam('displayName', true); displayName = qsParam("displayName", true);
avatarUrl = qsParam('avatarUrl', true); // http not mxc avatarUrl = qsParam("avatarUrl", true); // http not mxc
userId = qsParam('userId'); userId = qsParam("userId");
jitsiAuth = qsParam('auth', true); jitsiAuth = qsParam("auth", true);
roomId = qsParam('roomId', true); roomId = qsParam("roomId", true);
roomName = qsParam('roomName', true); roomName = qsParam("roomName", true);
startAudioOnly = qsParam('isAudioOnly', true) === "true"; startAudioOnly = qsParam("isAudioOnly", true) === "true";
isVideoChannel = qsParam('isVideoChannel', true) === "true"; isVideoChannel = qsParam("isVideoChannel", true) === "true";
supportsScreensharing = qsParam('supportsScreensharing', true) === "true"; supportsScreensharing = qsParam("supportsScreensharing", true) === "true";
// We've reached the point where we have to wait for the config, so do that then parse it. // We've reached the point where we have to wait for the config, so do that then parse it.
const instanceConfig = new SnakedObject<IConfigOptions>((await configPromise) ?? <IConfigOptions>{}); const instanceConfig = new SnakedObject<IConfigOptions>((await configPromise) ?? <IConfigOptions>{});
const jitsiConfig = instanceConfig.get("jitsi_widget") ?? {}; const jitsiConfig = instanceConfig.get("jitsi_widget") ?? {};
skipOurWelcomeScreen = (new SnakedObject<IConfigOptions["jitsi_widget"]>(jitsiConfig)) skipOurWelcomeScreen =
.get("skip_built_in_welcome_screen") ?? false; new SnakedObject<IConfigOptions["jitsi_widget"]>(jitsiConfig).get("skip_built_in_welcome_screen") ?? false;
// Either reveal the prejoin screen, or skip straight to Jitsi depending on the config. // Either reveal the prejoin screen, or skip straight to Jitsi depending on the config.
// We don't set up the call yet though as this might lead to failure without the widget API. // We don't set up the call yet though as this might lead to failure without the widget API.
@ -238,10 +238,10 @@ function switchVisibleContainers(): void {
} }
function toggleConferenceVisibility(inConference: boolean): void { function toggleConferenceVisibility(inConference: boolean): void {
document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden'; document.getElementById("jitsiContainer").style.visibility = inConference ? "unset" : "hidden";
// Video rooms have a separate UI for joining, so they should never show our join button // Video rooms have a separate UI for joining, so they should never show our join button
document.getElementById("joinButtonContainer").style.visibility = document.getElementById("joinButtonContainer").style.visibility =
(inConference || isVideoChannel) ? 'hidden' : 'unset'; inConference || isVideoChannel ? "hidden" : "unset";
} }
function skipToJitsiSplashScreen(): void { function skipToJitsiSplashScreen(): void {
@ -256,7 +256,7 @@ function skipToJitsiSplashScreen(): void {
*/ */
function createJWTToken(): string { function createJWTToken(): string {
// Header // Header
const header = { alg: 'HS256', typ: 'JWT' }; const header = { alg: "HS256", typ: "JWT" };
// Payload // Payload
const payload = { const payload = {
// As per Jitsi token auth, `iss` needs to be set to something agreed between // As per Jitsi token auth, `iss` needs to be set to something agreed between
@ -281,12 +281,7 @@ function createJWTToken(): string {
// Sign JWT // Sign JWT
// The secret string here is irrelevant, we're only using the JWT // The secret string here is irrelevant, we're only using the JWT
// to transport data to Prosody in the Jitsi stack. // to transport data to Prosody in the Jitsi stack.
return KJUR.jws.JWS.sign( return KJUR.jws.JWS.sign("HS256", JSON.stringify(header), JSON.stringify(payload), "notused");
'HS256',
JSON.stringify(header),
JSON.stringify(payload),
'notused',
);
} }
async function notifyHangup(errorMessage?: string): Promise<void> { async function notifyHangup(errorMessage?: string): Promise<void> {
@ -318,9 +313,10 @@ function closeConference(): void {
function joinConference(audioInput?: string | null, videoInput?: string | null): void { function joinConference(audioInput?: string | null, videoInput?: string | null): void {
let jwt; let jwt;
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) { if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
if (!openIdToken?.access_token) { // eslint-disable-line camelcase if (!openIdToken?.access_token) {
// eslint-disable-line camelcase
// We've failing to get a token, don't try to init conference // We've failing to get a token, don't try to init conference
logger.warn('Expected to have an OpenID credential, cannot initialize widget.'); logger.warn("Expected to have an OpenID credential, cannot initialize widget.");
document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget"; document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
return; return;
} }
@ -331,8 +327,8 @@ function joinConference(audioInput?: string | null, videoInput?: string | null):
logger.warn( logger.warn(
"[Jitsi Widget] The next few errors about failing to parse URL parameters are fine if " + "[Jitsi Widget] The next few errors about failing to parse URL parameters are fine if " +
"they mention 'external_api' or 'jitsi' in the stack. They're just Jitsi Meet trying to parse " + "they mention 'external_api' or 'jitsi' in the stack. They're just Jitsi Meet trying to parse " +
"our fragment values and not recognizing the options.", "our fragment values and not recognizing the options.",
); );
const options = { const options = {
@ -400,7 +396,7 @@ function joinConference(audioInput?: string | null, videoInput?: string | null):
meetApi.on("audioMuteStatusChanged", onAudioMuteStatusChanged); meetApi.on("audioMuteStatusChanged", onAudioMuteStatusChanged);
meetApi.on("videoMuteStatusChanged", onVideoMuteStatusChanged); meetApi.on("videoMuteStatusChanged", onVideoMuteStatusChanged);
["videoConferenceJoined", "participantJoined", "participantLeft"].forEach(event => { ["videoConferenceJoined", "participantJoined", "participantLeft"].forEach((event) => {
meetApi.on(event, updateParticipants); meetApi.on(event, updateParticipants);
}); });
@ -472,5 +468,4 @@ const updateParticipants = (): void => {
}); });
}; };
const onLog = ({ logLevel, args }): void => const onLog = ({ logLevel, args }): void => (parent as unknown as typeof global).mx_rage_logger?.log(logLevel, ...args);
(parent as unknown as typeof global).mx_rage_logger?.log(logLevel, ...args);

View File

@ -4,8 +4,8 @@
* */ * */
if (window.localStorage) { if (window.localStorage) {
Object.keys(window.localStorage).forEach(key => { Object.keys(window.localStorage).forEach((key) => {
if (key.indexOf('loglevel:') === 0) { if (key.indexOf("loglevel:") === 0) {
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
} }
}); });

File diff suppressed because one or more lines are too long

View File

@ -1,17 +1,17 @@
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { getVectorConfig } from '../getconfig'; import { getVectorConfig } from "../getconfig";
function onBackToElementClick(): void { function onBackToElementClick(): void {
// Cookie should expire in 4 hours // Cookie should expire in 4 hours
document.cookie = 'element_mobile_redirect_to_guide=false;path=/;max-age=14400'; document.cookie = "element_mobile_redirect_to_guide=false;path=/;max-age=14400";
window.location.href = '../'; window.location.href = "../";
} }
// NEVER pass user-controlled content to this function! Hardcoded strings only please. // NEVER pass user-controlled content to this function! Hardcoded strings only please.
function renderConfigError(message: string): void { function renderConfigError(message: string): void {
const contactMsg = "If this is unexpected, please contact your system administrator " + const contactMsg =
"or technical support representative."; "If this is unexpected, please contact your system administrator " + "or technical support representative.";
message = `<h2>Error loading Element</h2><p>${message}</p><p>${contactMsg}</p>`; message = `<h2>Error loading Element</h2><p>${message}</p><p>${contactMsg}</p>`;
const toHide = document.getElementsByClassName("mx_HomePage_container"); const toHide = document.getElementsByClassName("mx_HomePage_container");
@ -22,46 +22,46 @@ function renderConfigError(message: string): void {
for (const e of toHide) { for (const e of toHide) {
// We have to clear the content because .style.display='none'; doesn't work // We have to clear the content because .style.display='none'; doesn't work
// due to an !important in the CSS. // due to an !important in the CSS.
e.innerHTML = ''; e.innerHTML = "";
} }
for (const e of errorContainers) { for (const e of errorContainers) {
e.style.display = 'block'; e.style.display = "block";
e.innerHTML = message; e.innerHTML = message;
} }
} }
async function initPage(): Promise<void> { async function initPage(): Promise<void> {
document.getElementById('back_to_element_button').onclick = onBackToElementClick; document.getElementById("back_to_element_button").onclick = onBackToElementClick;
const config = await getVectorConfig('..'); const config = await getVectorConfig("..");
// We manually parse the config similar to how validateServerConfig works because // We manually parse the config similar to how validateServerConfig works because
// calling that function pulls in roughly 4mb of JS we don't use. // calling that function pulls in roughly 4mb of JS we don't use.
const wkConfig = config['default_server_config']; // overwritten later under some conditions const wkConfig = config["default_server_config"]; // overwritten later under some conditions
const serverName = config['default_server_name']; const serverName = config["default_server_name"];
const defaultHsUrl = config['default_hs_url']; const defaultHsUrl = config["default_hs_url"];
const defaultIsUrl = config['default_is_url']; const defaultIsUrl = config["default_is_url"];
const incompatibleOptions = [wkConfig, serverName, defaultHsUrl].filter(i => !!i); const incompatibleOptions = [wkConfig, serverName, defaultHsUrl].filter((i) => !!i);
if (incompatibleOptions.length > 1) { if (incompatibleOptions.length > 1) {
return renderConfigError( return renderConfigError(
"Invalid configuration: can only specify one of default_server_config, default_server_name, " + "Invalid configuration: can only specify one of default_server_config, default_server_name, " +
"or default_hs_url.", "or default_hs_url.",
); );
} }
if (incompatibleOptions.length < 1) { if (incompatibleOptions.length < 1) {
return renderConfigError("Invalid configuration: no default server specified."); return renderConfigError("Invalid configuration: no default server specified.");
} }
let hsUrl = ''; let hsUrl = "";
let isUrl = ''; let isUrl = "";
if (wkConfig && wkConfig['m.homeserver']) { if (wkConfig && wkConfig["m.homeserver"]) {
hsUrl = wkConfig['m.homeserver']['base_url']; hsUrl = wkConfig["m.homeserver"]["base_url"];
if (wkConfig['m.identity_server']) { if (wkConfig["m.identity_server"]) {
isUrl = wkConfig['m.identity_server']['base_url']; isUrl = wkConfig["m.identity_server"]["base_url"];
} }
} }
@ -70,11 +70,11 @@ async function initPage(): Promise<void> {
try { try {
const result = await fetch(`https://${serverName}/.well-known/matrix/client`); const result = await fetch(`https://${serverName}/.well-known/matrix/client`);
const wkConfig = await result.json(); const wkConfig = await result.json();
if (wkConfig && wkConfig['m.homeserver']) { if (wkConfig && wkConfig["m.homeserver"]) {
hsUrl = wkConfig['m.homeserver']['base_url']; hsUrl = wkConfig["m.homeserver"]["base_url"];
if (wkConfig['m.identity_server']) { if (wkConfig["m.identity_server"]) {
isUrl = wkConfig['m.identity_server']['base_url']; isUrl = wkConfig["m.identity_server"]["base_url"];
} }
} }
} catch (e) { } catch (e) {
@ -92,21 +92,20 @@ async function initPage(): Promise<void> {
return renderConfigError("Unable to locate homeserver"); return renderConfigError("Unable to locate homeserver");
} }
if (hsUrl && !hsUrl.endsWith('/')) hsUrl += '/'; if (hsUrl && !hsUrl.endsWith("/")) hsUrl += "/";
if (isUrl && !isUrl.endsWith('/')) isUrl += '/'; if (isUrl && !isUrl.endsWith("/")) isUrl += "/";
if (hsUrl !== 'https://matrix.org/') { if (hsUrl !== "https://matrix.org/") {
(document.getElementById('configure_element_button') as HTMLAnchorElement).href = (document.getElementById("configure_element_button") as HTMLAnchorElement).href =
"https://mobile.element.io?hs_url=" + encodeURIComponent(hsUrl) + "https://mobile.element.io?hs_url=" + encodeURIComponent(hsUrl) + "&is_url=" + encodeURIComponent(isUrl);
"&is_url=" + encodeURIComponent(isUrl); document.getElementById("step1_heading").innerHTML = "1: Install the app";
document.getElementById('step1_heading').innerHTML= '1: Install the app'; document.getElementById("step2_container").style.display = "block";
document.getElementById('step2_container').style.display = 'block'; document.getElementById("hs_url").innerText = hsUrl;
document.getElementById('hs_url').innerText = hsUrl;
if (isUrl) { if (isUrl) {
document.getElementById('custom_is').style.display = 'block'; document.getElementById("custom_is").style.display = "block";
document.getElementById('is_url').style.display = 'block'; document.getElementById("is_url").style.display = "block";
document.getElementById('is_url').innerText = isUrl; document.getElementById("is_url").innerText = isUrl;
} }
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -19,12 +19,12 @@ limitations under the License.
*/ */
import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform"; import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform";
import BaseEventIndexManager from 'matrix-react-sdk/src/indexing/BaseEventIndexManager'; import BaseEventIndexManager from "matrix-react-sdk/src/indexing/BaseEventIndexManager";
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher'; import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
import { _t } from 'matrix-react-sdk/src/languageHandler'; import { _t } from "matrix-react-sdk/src/languageHandler";
import SdkConfig from 'matrix-react-sdk/src/SdkConfig'; import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions"; import { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
import * as rageshake from 'matrix-react-sdk/src/rageshake/rageshake'; import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import Modal from "matrix-react-sdk/src/Modal"; import Modal from "matrix-react-sdk/src/Modal";
@ -41,35 +41,35 @@ import GenericExpiringToast from "matrix-react-sdk/src/components/views/toasts/G
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import VectorBasePlatform from './VectorBasePlatform'; import VectorBasePlatform from "./VectorBasePlatform";
import { SeshatIndexManager } from "./SeshatIndexManager"; import { SeshatIndexManager } from "./SeshatIndexManager";
import { IPCManager } from "./IPCManager"; import { IPCManager } from "./IPCManager";
const isMac = navigator.platform.toUpperCase().includes('MAC'); const isMac = navigator.platform.toUpperCase().includes("MAC");
function platformFriendlyName(): string { function platformFriendlyName(): string {
// used to use window.process but the same info is available here // used to use window.process but the same info is available here
if (navigator.userAgent.includes('Macintosh')) { if (navigator.userAgent.includes("Macintosh")) {
return 'macOS'; return "macOS";
} else if (navigator.userAgent.includes('FreeBSD')) { } else if (navigator.userAgent.includes("FreeBSD")) {
return 'FreeBSD'; return "FreeBSD";
} else if (navigator.userAgent.includes('OpenBSD')) { } else if (navigator.userAgent.includes("OpenBSD")) {
return 'OpenBSD'; return "OpenBSD";
} else if (navigator.userAgent.includes('SunOS')) { } else if (navigator.userAgent.includes("SunOS")) {
return 'SunOS'; return "SunOS";
} else if (navigator.userAgent.includes('Windows')) { } else if (navigator.userAgent.includes("Windows")) {
return 'Windows'; return "Windows";
} else if (navigator.userAgent.includes('Linux')) { } else if (navigator.userAgent.includes("Linux")) {
return 'Linux'; return "Linux";
} else { } else {
return 'Unknown'; return "Unknown";
} }
} }
function onAction(payload: ActionPayload): void { function onAction(payload: ActionPayload): void {
// Whitelist payload actions, no point sending most across // Whitelist payload actions, no point sending most across
if (['call_state'].includes(payload.action)) { if (["call_state"].includes(payload.action)) {
window.electron.send('app_onAction', payload); window.electron.send("app_onAction", payload);
} }
} }
@ -102,7 +102,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
false if there is not false if there is not
or the error if one is encountered or the error if one is encountered
*/ */
window.electron.on('check_updates', (event, status) => { window.electron.on("check_updates", (event, status) => {
dis.dispatch<CheckUpdatesPayload>({ dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates, action: Action.CheckUpdates,
...getUpdateCheckStatus(status), ...getUpdateCheckStatus(status),
@ -110,27 +110,27 @@ export default class ElectronPlatform extends VectorBasePlatform {
}); });
// try to flush the rageshake logs to indexeddb before quit. // try to flush the rageshake logs to indexeddb before quit.
window.electron.on('before-quit', function() { window.electron.on("before-quit", function () {
logger.log('element-desktop closing'); logger.log("element-desktop closing");
rageshake.flush(); rageshake.flush();
}); });
window.electron.on('update-downloaded', this.onUpdateDownloaded); window.electron.on("update-downloaded", this.onUpdateDownloaded);
window.electron.on('preferences', () => { window.electron.on("preferences", () => {
dis.fire(Action.ViewUserSettings); dis.fire(Action.ViewUserSettings);
}); });
window.electron.on('userDownloadCompleted', (ev, { id, name }) => { window.electron.on("userDownloadCompleted", (ev, { id, name }) => {
const key = `DOWNLOAD_TOAST_${id}`; const key = `DOWNLOAD_TOAST_${id}`;
const onAccept = (): void => { const onAccept = (): void => {
window.electron.send('userDownloadAction', { id, open: true }); window.electron.send("userDownloadAction", { id, open: true });
ToastStore.sharedInstance().dismissToast(key); ToastStore.sharedInstance().dismissToast(key);
}; };
const onDismiss = (): void => { const onDismiss = (): void => {
window.electron.send('userDownloadAction', { id }); window.electron.send("userDownloadAction", { id });
}; };
ToastStore.sharedInstance().addOrReplaceToast({ ToastStore.sharedInstance().addOrReplaceToast({
@ -153,7 +153,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
} }
public async getConfig(): Promise<IConfigOptions> { public async getConfig(): Promise<IConfigOptions> {
return this.ipc.call('getConfig'); return this.ipc.call("getConfig");
} }
private onUpdateDownloaded = async (ev, { releaseNotes, releaseName }): Promise<void> => { private onUpdateDownloaded = async (ev, { releaseNotes, releaseName }): Promise<void> => {
@ -167,7 +167,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
}; };
public getHumanReadableName(): string { public getHumanReadableName(): string {
return 'Electron Platform'; // no translation required: only used for analytics return "Electron Platform"; // no translation required: only used for analytics
} }
/** /**
@ -186,7 +186,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
if (this.notificationCount === count) return; if (this.notificationCount === count) return;
super.setNotificationCount(count); super.setNotificationCount(count);
window.electron.send('setBadgeCount', count); window.electron.send("setBadgeCount", count);
} }
public supportsNotifications(): boolean { public supportsNotifications(): boolean {
@ -210,29 +210,23 @@ export default class ElectronPlatform extends VectorBasePlatform {
// maybe we should pass basic styling (italics, bold, underline) through from MD // maybe we should pass basic styling (italics, bold, underline) through from MD
// we only have to strip out < and > as the spec doesn't include anything about things like &amp; // we only have to strip out < and > as the spec doesn't include anything about things like &amp;
// so we shouldn't assume that all implementations will treat those properly. Very basic tag parsing is done. // so we shouldn't assume that all implementations will treat those properly. Very basic tag parsing is done.
if (navigator.userAgent.includes('Linux')) { if (navigator.userAgent.includes("Linux")) {
msg = msg.replace(/</g, '&lt;').replace(/>/g, '&gt;'); msg = msg.replace(/</g, "&lt;").replace(/>/g, "&gt;");
} }
const notification = super.displayNotification( const notification = super.displayNotification(title, msg, avatarUrl, room, ev);
title,
msg,
avatarUrl,
room,
ev,
);
const handler = notification.onclick as Function; const handler = notification.onclick as Function;
notification.onclick = (): void => { notification.onclick = (): void => {
handler?.(); handler?.();
this.ipc.call('focusWindow'); this.ipc.call("focusWindow");
}; };
return notification; return notification;
} }
public loudNotification(ev: MatrixEvent, room: Room): void { public loudNotification(ev: MatrixEvent, room: Room): void {
window.electron.send('loudNotification'); window.electron.send("loudNotification");
} }
public needsUrlTooltips(): boolean { public needsUrlTooltips(): boolean {
@ -240,7 +234,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
} }
public async getAppVersion(): Promise<string> { public async getAppVersion(): Promise<string> {
return this.ipc.call('getAppVersion'); return this.ipc.call("getAppVersion");
} }
public supportsSetting(settingName?: string): boolean { public supportsSetting(settingName?: string): boolean {
@ -262,32 +256,32 @@ export default class ElectronPlatform extends VectorBasePlatform {
} }
public async canSelfUpdate(): Promise<boolean> { public async canSelfUpdate(): Promise<boolean> {
const feedUrl = await this.ipc.call('getUpdateFeedUrl'); const feedUrl = await this.ipc.call("getUpdateFeedUrl");
return Boolean(feedUrl); return Boolean(feedUrl);
} }
public startUpdateCheck(): void { public startUpdateCheck(): void {
super.startUpdateCheck(); super.startUpdateCheck();
window.electron.send('check_updates'); window.electron.send("check_updates");
} }
public installUpdate(): void { public installUpdate(): void {
// IPC to the main process to install the update, since quitAndInstall // IPC to the main process to install the update, since quitAndInstall
// doesn't fire the before-quit event so the main process needs to know // doesn't fire the before-quit event so the main process needs to know
// it should exit. // it should exit.
window.electron.send('install_update'); window.electron.send("install_update");
} }
public getDefaultDeviceDisplayName(): string { public getDefaultDeviceDisplayName(): string {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
return _t('%(brand)s Desktop: %(platformName)s', { return _t("%(brand)s Desktop: %(platformName)s", {
brand, brand,
platformName: platformFriendlyName(), platformName: platformFriendlyName(),
}); });
} }
public requestNotificationPermission(): Promise<string> { public requestNotificationPermission(): Promise<string> {
return Promise.resolve('granted'); return Promise.resolve("granted");
} }
public reload(): void { public reload(): void {
@ -299,33 +293,33 @@ export default class ElectronPlatform extends VectorBasePlatform {
} }
public async setLanguage(preferredLangs: string[]): Promise<any> { public async setLanguage(preferredLangs: string[]): Promise<any> {
return this.ipc.call('setLanguage', preferredLangs); return this.ipc.call("setLanguage", preferredLangs);
} }
public setSpellCheckEnabled(enabled: boolean): void { public setSpellCheckEnabled(enabled: boolean): void {
this.ipc.call('setSpellCheckEnabled', enabled).catch(error => { this.ipc.call("setSpellCheckEnabled", enabled).catch((error) => {
logger.log("Failed to send setSpellCheckEnabled IPC to Electron"); logger.log("Failed to send setSpellCheckEnabled IPC to Electron");
logger.error(error); logger.error(error);
}); });
} }
public async getSpellCheckEnabled(): Promise<boolean> { public async getSpellCheckEnabled(): Promise<boolean> {
return this.ipc.call('getSpellCheckEnabled'); return this.ipc.call("getSpellCheckEnabled");
} }
public setSpellCheckLanguages(preferredLangs: string[]): void { public setSpellCheckLanguages(preferredLangs: string[]): void {
this.ipc.call('setSpellCheckLanguages', preferredLangs).catch(error => { this.ipc.call("setSpellCheckLanguages", preferredLangs).catch((error) => {
logger.log("Failed to send setSpellCheckLanguages IPC to Electron"); logger.log("Failed to send setSpellCheckLanguages IPC to Electron");
logger.error(error); logger.error(error);
}); });
} }
public async getSpellCheckLanguages(): Promise<string[]> { public async getSpellCheckLanguages(): Promise<string[]> {
return this.ipc.call('getSpellCheckLanguages'); return this.ipc.call("getSpellCheckLanguages");
} }
public async getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>> { public async getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>> {
return this.ipc.call('getDesktopCapturerSources', options); return this.ipc.call("getDesktopCapturerSources", options);
} }
public supportsDesktopCapturer(): boolean { public supportsDesktopCapturer(): boolean {
@ -338,7 +332,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
} }
public async getAvailableSpellCheckLanguages(): Promise<string[]> { public async getAvailableSpellCheckLanguages(): Promise<string[]> {
return this.ipc.call('getAvailableSpellCheckLanguages'); return this.ipc.call("getAvailableSpellCheckLanguages");
} }
public getSSOCallbackUrl(fragmentAfterLogin: string): URL { public getSSOCallbackUrl(fragmentAfterLogin: string): URL {
@ -372,7 +366,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
public async getPickleKey(userId: string, deviceId: string): Promise<string | null> { public async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
try { try {
return await this.ipc.call('getPickleKey', userId, deviceId); return await this.ipc.call("getPickleKey", userId, deviceId);
} catch (e) { } catch (e) {
// if we can't connect to the password storage, assume there's no // if we can't connect to the password storage, assume there's no
// pickle key // pickle key
@ -382,7 +376,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
public async createPickleKey(userId: string, deviceId: string): Promise<string | null> { public async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
try { try {
return await this.ipc.call('createPickleKey', userId, deviceId); return await this.ipc.call("createPickleKey", userId, deviceId);
} catch (e) { } catch (e) {
// if we can't connect to the password storage, assume there's no // if we can't connect to the password storage, assume there's no
// pickle key // pickle key
@ -392,7 +386,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
public async destroyPickleKey(userId: string, deviceId: string): Promise<void> { public async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
try { try {
await this.ipc.call('destroyPickleKey', userId, deviceId); await this.ipc.call("destroyPickleKey", userId, deviceId);
} catch (e) {} } catch (e) {}
} }
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { defer, IDeferred } from 'matrix-js-sdk/src/utils'; import { defer, IDeferred } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ElectronChannel } from "../../@types/global"; import { ElectronChannel } from "../../@types/global";

View File

@ -24,7 +24,7 @@ export default class PWAPlatform extends WebPlatform {
if (this.notificationCount === count) return; if (this.notificationCount === count) return;
this.notificationCount = count; this.notificationCount = count;
navigator.setAppBadge(count).catch(e => { navigator.setAppBadge(count).catch((e) => {
logger.error("Failed to update PWA app badge", e); logger.error("Failed to update PWA app badge", e);
}); });
} }

View File

@ -19,7 +19,7 @@ import BaseEventIndexManager, {
IEventAndProfile, IEventAndProfile,
IIndexStats, IIndexStats,
ISearchArgs, ISearchArgs,
} from 'matrix-react-sdk/src/indexing/BaseEventIndexManager'; } from "matrix-react-sdk/src/indexing/BaseEventIndexManager";
import { IMatrixProfile, IEventWithRoomId as IMatrixEvent, IResultRoomEvents } from "matrix-js-sdk/src/@types/search"; import { IMatrixProfile, IEventWithRoomId as IMatrixEvent, IResultRoomEvents } from "matrix-js-sdk/src/@types/search";
import { IPCManager } from "./IPCManager"; import { IPCManager } from "./IPCManager";
@ -28,35 +28,35 @@ export class SeshatIndexManager extends BaseEventIndexManager {
private readonly ipc = new IPCManager("seshat", "seshatReply"); private readonly ipc = new IPCManager("seshat", "seshatReply");
public async supportsEventIndexing(): Promise<boolean> { public async supportsEventIndexing(): Promise<boolean> {
return this.ipc.call('supportsEventIndexing'); return this.ipc.call("supportsEventIndexing");
} }
public async initEventIndex(userId: string, deviceId: string): Promise<void> { public async initEventIndex(userId: string, deviceId: string): Promise<void> {
return this.ipc.call('initEventIndex', userId, deviceId); return this.ipc.call("initEventIndex", userId, deviceId);
} }
public async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> { public async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> {
return this.ipc.call('addEventToIndex', ev, profile); return this.ipc.call("addEventToIndex", ev, profile);
} }
public async deleteEvent(eventId: string): Promise<boolean> { public async deleteEvent(eventId: string): Promise<boolean> {
return this.ipc.call('deleteEvent', eventId); return this.ipc.call("deleteEvent", eventId);
} }
public async isEventIndexEmpty(): Promise<boolean> { public async isEventIndexEmpty(): Promise<boolean> {
return this.ipc.call('isEventIndexEmpty'); return this.ipc.call("isEventIndexEmpty");
} }
public async isRoomIndexed(roomId: string): Promise<boolean> { public async isRoomIndexed(roomId: string): Promise<boolean> {
return this.ipc.call('isRoomIndexed', roomId); return this.ipc.call("isRoomIndexed", roomId);
} }
public async commitLiveEvents(): Promise<void> { public async commitLiveEvents(): Promise<void> {
return this.ipc.call('commitLiveEvents'); return this.ipc.call("commitLiveEvents");
} }
public async searchEventIndex(searchConfig: ISearchArgs): Promise<IResultRoomEvents> { public async searchEventIndex(searchConfig: ISearchArgs): Promise<IResultRoomEvents> {
return this.ipc.call('searchEventIndex', searchConfig); return this.ipc.call("searchEventIndex", searchConfig);
} }
public async addHistoricEvents( public async addHistoricEvents(
@ -64,42 +64,42 @@ export class SeshatIndexManager extends BaseEventIndexManager {
checkpoint: ICrawlerCheckpoint | null, checkpoint: ICrawlerCheckpoint | null,
oldCheckpoint: ICrawlerCheckpoint | null, oldCheckpoint: ICrawlerCheckpoint | null,
): Promise<boolean> { ): Promise<boolean> {
return this.ipc.call('addHistoricEvents', events, checkpoint, oldCheckpoint); return this.ipc.call("addHistoricEvents", events, checkpoint, oldCheckpoint);
} }
public async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> { public async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
return this.ipc.call('addCrawlerCheckpoint', checkpoint); return this.ipc.call("addCrawlerCheckpoint", checkpoint);
} }
public async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> { public async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
return this.ipc.call('removeCrawlerCheckpoint', checkpoint); return this.ipc.call("removeCrawlerCheckpoint", checkpoint);
} }
public async loadFileEvents(args): Promise<IEventAndProfile[]> { public async loadFileEvents(args): Promise<IEventAndProfile[]> {
return this.ipc.call('loadFileEvents', args); return this.ipc.call("loadFileEvents", args);
} }
public async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> { public async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> {
return this.ipc.call('loadCheckpoints'); return this.ipc.call("loadCheckpoints");
} }
public async closeEventIndex(): Promise<void> { public async closeEventIndex(): Promise<void> {
return this.ipc.call('closeEventIndex'); return this.ipc.call("closeEventIndex");
} }
public async getStats(): Promise<IIndexStats> { public async getStats(): Promise<IIndexStats> {
return this.ipc.call('getStats'); return this.ipc.call("getStats");
} }
public async getUserVersion(): Promise<number> { public async getUserVersion(): Promise<number> {
return this.ipc.call('getUserVersion'); return this.ipc.call("getUserVersion");
} }
public async setUserVersion(version: number): Promise<void> { public async setUserVersion(version: number): Promise<void> {
return this.ipc.call('setUserVersion', version); return this.ipc.call("setUserVersion", version);
} }
public async deleteEventIndex(): Promise<void> { public async deleteEventIndex(): Promise<void> {
return this.ipc.call('deleteEventIndex'); return this.ipc.call("deleteEventIndex");
} }
} }

View File

@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import BasePlatform from 'matrix-react-sdk/src/BasePlatform'; import BasePlatform from "matrix-react-sdk/src/BasePlatform";
import { _t } from 'matrix-react-sdk/src/languageHandler'; import { _t } from "matrix-react-sdk/src/languageHandler";
import type { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions"; import type { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
import { getVectorConfig } from "../getconfig"; import { getVectorConfig } from "../getconfig";
@ -35,7 +35,7 @@ export default abstract class VectorBasePlatform extends BasePlatform {
} }
public getHumanReadableName(): string { public getHumanReadableName(): string {
return 'Vector Base Platform'; // no translation required: only used for analytics return "Vector Base Platform"; // no translation required: only used for analytics
} }
/** /**
@ -78,8 +78,7 @@ export default abstract class VectorBasePlatform extends BasePlatform {
/** /**
* Begin update polling, if applicable * Begin update polling, if applicable
*/ */
public startUpdater(): void { public startUpdater(): void {}
}
/** /**
* Get a sensible default display name for the * Get a sensible default display name for the

View File

@ -17,15 +17,15 @@ limitations under the License.
*/ */
import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform"; import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform";
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher'; import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
import { _t } from 'matrix-react-sdk/src/languageHandler'; import { _t } from "matrix-react-sdk/src/languageHandler";
import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "matrix-react-sdk/src/toasts/UpdateToast"; import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "matrix-react-sdk/src/toasts/UpdateToast";
import { Action } from "matrix-react-sdk/src/dispatcher/actions"; import { Action } from "matrix-react-sdk/src/dispatcher/actions";
import { CheckUpdatesPayload } from 'matrix-react-sdk/src/dispatcher/payloads/CheckUpdatesPayload'; import { CheckUpdatesPayload } from "matrix-react-sdk/src/dispatcher/payloads/CheckUpdatesPayload";
import UAParser from 'ua-parser-js'; import UAParser from "ua-parser-js";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import VectorBasePlatform from './VectorBasePlatform'; import VectorBasePlatform from "./VectorBasePlatform";
import { parseQs } from "../url_utils"; import { parseQs } from "../url_utils";
const POKE_RATE_MS = 10 * 60 * 1000; // 10 min const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
@ -43,13 +43,13 @@ export default class WebPlatform extends VectorBasePlatform {
public constructor() { public constructor() {
super(); super();
// Register service worker if available on this platform // Register service worker if available on this platform
if ('serviceWorker' in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register('sw.js'); navigator.serviceWorker.register("sw.js");
} }
} }
public getHumanReadableName(): string { public getHumanReadableName(): string {
return 'Web Platform'; // no translation required: only used for analytics return "Web Platform"; // no translation required: only used for analytics
} }
/** /**
@ -65,7 +65,7 @@ export default class WebPlatform extends VectorBasePlatform {
* to display notifications. Otherwise false. * to display notifications. Otherwise false.
*/ */
public maySendNotifications(): boolean { public maySendNotifications(): boolean {
return window.Notification.permission === 'granted'; return window.Notification.permission === "granted";
} }
/** /**
@ -79,7 +79,7 @@ export default class WebPlatform extends VectorBasePlatform {
// annoyingly, the latest spec says this returns a // annoyingly, the latest spec says this returns a
// promise, but this is only supported in Chrome 46 // promise, but this is only supported in Chrome 46
// and Firefox 47, so adapt the callback API. // and Firefox 47, so adapt the callback API.
return new Promise(function(resolve) { return new Promise(function (resolve) {
window.Notification.requestPermission((result) => { window.Notification.requestPermission((result) => {
resolve(result); resolve(result);
}); });
@ -142,30 +142,33 @@ export default class WebPlatform extends VectorBasePlatform {
showUpdate: (currentVersion: string, mostRecentVersion: string) => void, showUpdate: (currentVersion: string, mostRecentVersion: string) => void,
showNoUpdate?: () => void, showNoUpdate?: () => void,
): Promise<UpdateStatus> => { ): Promise<UpdateStatus> => {
return this.getMostRecentVersion().then((mostRecentVersion) => { return this.getMostRecentVersion().then(
const currentVersion = getNormalizedAppVersion(process.env.VERSION); (mostRecentVersion) => {
const currentVersion = getNormalizedAppVersion(process.env.VERSION);
if (currentVersion !== mostRecentVersion) { if (currentVersion !== mostRecentVersion) {
if (this.shouldShowUpdate(mostRecentVersion)) { if (this.shouldShowUpdate(mostRecentVersion)) {
console.log("Update available to " + mostRecentVersion + ", will notify user"); console.log("Update available to " + mostRecentVersion + ", will notify user");
showUpdate(currentVersion, mostRecentVersion); showUpdate(currentVersion, mostRecentVersion);
} else {
console.log("Update available to " + mostRecentVersion + " but won't be shown");
}
return { status: UpdateCheckStatus.Ready };
} else { } else {
console.log("Update available to " + mostRecentVersion + " but won't be shown"); console.log("No update available, already on " + mostRecentVersion);
showNoUpdate?.();
} }
return { status: UpdateCheckStatus.Ready };
} else {
console.log("No update available, already on " + mostRecentVersion);
showNoUpdate?.();
}
return { status: UpdateCheckStatus.NotAvailable }; return { status: UpdateCheckStatus.NotAvailable };
}, (err) => { },
logger.error("Failed to poll for update", err); (err) => {
return { logger.error("Failed to poll for update", err);
status: UpdateCheckStatus.Error, return {
detail: err.message || err.status ? err.status.toString() : 'Unknown Error', status: UpdateCheckStatus.Error,
}; detail: err.message || err.status ? err.status.toString() : "Unknown Error",
}); };
},
);
}; };
public startUpdateCheck(): void { public startUpdateCheck(): void {
@ -197,7 +200,7 @@ export default class WebPlatform extends VectorBasePlatform {
let osName = ua.getOS().name || "unknown OS"; let osName = ua.getOS().name || "unknown OS";
// Stylise the value from the parser to match Apple's current branding. // Stylise the value from the parser to match Apple's current branding.
if (osName === "Mac OS") osName = "macOS"; if (osName === "Mac OS") osName = "macOS";
return _t('%(appName)s: %(browserName)s on %(osName)s', { return _t("%(appName)s: %(browserName)s on %(osName)s", {
appName, appName,
browserName, browserName,
osName, osName,

View File

@ -33,22 +33,26 @@ import { logger } from "matrix-js-sdk/src/logger";
export function initRageshake(): Promise<void> { export function initRageshake(): Promise<void> {
// we manually check persistence for rageshakes ourselves // we manually check persistence for rageshakes ourselves
const prom = rageshake.init(/*setUpPersistence=*/false); const prom = rageshake.init(/*setUpPersistence=*/ false);
prom.then(() => { prom.then(
logger.log("Initialised rageshake."); () => {
logger.log("To fix line numbers in Chrome: " + logger.log("Initialised rageshake.");
"Meatball menu → Settings → Ignore list → Add /rageshake\\.js$"); logger.log(
"To fix line numbers in Chrome: " + "Meatball menu → Settings → Ignore list → Add /rageshake\\.js$",
);
window.addEventListener('beforeunload', () => { window.addEventListener("beforeunload", () => {
logger.log('element-web closing'); logger.log("element-web closing");
// try to flush the logs to indexeddb // try to flush the logs to indexeddb
rageshake.flush(); rageshake.flush();
}); });
rageshake.cleanup(); rageshake.cleanup();
}, (err) => { },
logger.error("Failed to initialise rageshake: " + err); (err) => {
}); logger.error("Failed to initialise rageshake: " + err);
},
);
return prom; return prom;
} }
@ -56,7 +60,7 @@ export function initRageshakeStore(): Promise<void> {
return rageshake.tryInitStorage(); return rageshake.tryInitStorage();
} }
window.mxSendRageshake = function(text: string, withLogs?: boolean): void { window.mxSendRageshake = function (text: string, withLogs?: boolean): void {
const url = SdkConfig.get().bug_report_endpoint_url; const url = SdkConfig.get().bug_report_endpoint_url;
if (!url) { if (!url) {
logger.error("Cannot send a rageshake - no bug_report_endpoint_url configured"); logger.error("Cannot send a rageshake - no bug_report_endpoint_url configured");
@ -72,9 +76,12 @@ window.mxSendRageshake = function(text: string, withLogs?: boolean): void {
userText: text, userText: text,
sendLogs: withLogs, sendLogs: withLogs,
progressCallback: logger.log.bind(console), progressCallback: logger.log.bind(console),
}).then(() => { }).then(
logger.log("Bug report sent!"); () => {
}, (err) => { logger.log("Bug report sent!");
logger.error(err); },
}); (err) => {
logger.error(err);
},
);
}; };

View File

@ -24,7 +24,7 @@ import { parseQsFromFragment } from "./url_utils";
let lastLocationHashSet: string = null; let lastLocationHashSet: string = null;
export function getScreenFromLocation(location: Location): { screen: string, params: QueryDict } { export function getScreenFromLocation(location: Location): { screen: string; params: QueryDict } {
const fragparts = parseQsFromFragment(location); const fragparts = parseQsFromFragment(location);
return { return {
screen: fragparts.location.substring(1), screen: fragparts.location.substring(1),
@ -54,11 +54,12 @@ function onHashChange(): void {
// so a web page can update the URL bar appropriately. // so a web page can update the URL bar appropriately.
export function onNewScreen(screen: string, replaceLast = false): void { export function onNewScreen(screen: string, replaceLast = false): void {
logger.log("newscreen " + screen); logger.log("newscreen " + screen);
const hash = '#/' + screen; const hash = "#/" + screen;
lastLocationHashSet = hash; lastLocationHashSet = hash;
// if the new hash is a substring of the old one then we are stripping fields e.g `via` so replace history // if the new hash is a substring of the old one then we are stripping fields e.g `via` so replace history
if (screen.startsWith("room/") && if (
screen.startsWith("room/") &&
window.location.hash.includes("/$") === hash.includes("/$") && // only if both did or didn't contain event link window.location.hash.includes("/$") === hash.includes("/$") && // only if both did or didn't contain event link
window.location.hash.startsWith(hash) window.location.hash.startsWith(hash)
) { ) {
@ -73,5 +74,5 @@ export function onNewScreen(screen: string, replaceLast = false): void {
} }
export function init(): void { export function init(): void {
window.addEventListener('hashchange', onHashChange); window.addEventListener("hashchange", onHashChange);
} }

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head> <head>
<style type="text/css"> <style type="text/css">
/* By default, hide the custom IS stuff - enabled in JS */ /* By default, hide the custom IS stuff - enabled in JS */
#custom_is, #is_url { #custom_is,
#is_url {
display: none; display: none;
} }
@ -17,7 +16,8 @@
background: #f9fafb; background: #f9fafb;
max-width: 680px; max-width: 680px;
margin: auto; margin: auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
} }
.mx_Button { .mx_Button {
@ -27,7 +27,7 @@
margin-left: 4px; margin-left: 4px;
margin-right: 4px; margin-right: 4px;
min-width: 80px; min-width: 80px;
background-color: #03B381; background-color: #03b381;
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
padding: 12px 22px; padding: 12px 22px;
@ -44,7 +44,7 @@
} }
.mx_HomePage_header { .mx_HomePage_header {
color: #2E2F32; color: #2e2f32;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -72,7 +72,7 @@
} }
.mx_HomePage_container { .mx_HomePage_container {
display: block ! important; display: block !important;
margin: 10px 20px; margin: 10px 20px;
} }
@ -137,43 +137,58 @@
} }
</style> </style>
<meta name="apple-itunes-app" content="app-id=id1083446067"> <meta name="apple-itunes-app" content="app-id=id1083446067" />
</head> </head>
<body> <body>
<div class="mx_HomePage_errorContainer">
<div class="mx_HomePage_errorContainer"> <!-- populated by JS if needed -->
<!-- populated by JS if needed -->
</div>
<div class="mx_HomePage_container">
<div class="mx_HomePage_header">
<span class="mx_HomePage_logo">
<svg width="34" height="42" viewBox="0 0 34 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.08 8.68C13.08 7.75216 13.8321 7 14.76 7C20.9456 7 25.96 12.0144 25.96 18.2C25.96 19.1278 25.2078 19.88 24.28 19.88C23.3521 19.88 22.6 19.1278 22.6 18.2C22.6 13.8701 19.0899 10.36 14.76 10.36C13.8321 10.36 13.08 9.60784 13.08 8.68Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.92 33.32C20.92 34.2478 20.1679 35 19.24 35C13.0544 35 8.04001 29.9856 8.04001 23.8C8.04001 22.8722 8.79217 22.12 9.72001 22.12C10.6478 22.12 11.4 22.8722 11.4 23.8C11.4 28.1299 14.9101 31.64 19.24 31.64C20.1679 31.64 20.92 32.3922 20.92 33.32Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.68 24.9199C3.75216 24.9199 3 24.1678 3 23.2399C3 17.0543 8.01441 12.0399 14.2 12.0399C15.1278 12.0399 15.88 12.7921 15.88 13.7199C15.88 14.6478 15.1278 15.3999 14.2 15.3999C9.87009 15.3999 6.36 18.91 6.36 23.2399C6.36 24.1678 5.60784 24.9199 4.68 24.9199Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.32 17.0801C30.2478 17.0801 31 17.8322 31 18.7601C31 24.9457 25.9856 29.9601 19.8 29.9601C18.8722 29.9601 18.12 29.2079 18.12 28.2801C18.12 27.3522 18.8722 26.6001 19.8 26.6001C24.1299 26.6001 27.64 23.09 27.64 18.7601C27.64 17.8322 28.3922 17.0801 29.32 17.0801Z" fill="#0DBD8B"/>
</svg>
</span>
<h1>Unable to load</h1>
</div> </div>
<div class="mx_HomePage_col">
<div class="mx_HomePage_row"> <div class="mx_HomePage_container">
<div> <div class="mx_HomePage_header">
<h2 id="step1_heading">Element can't load</h2> <span class="mx_HomePage_logo">
<p>Something went wrong and Element was unable to load.</p> <svg width="34" height="42" viewBox="0 0 34 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.08 8.68C13.08 7.75216 13.8321 7 14.76 7C20.9456 7 25.96 12.0144 25.96 18.2C25.96 19.1278 25.2078 19.88 24.28 19.88C23.3521 19.88 22.6 19.1278 22.6 18.2C22.6 13.8701 19.0899 10.36 14.76 10.36C13.8321 10.36 13.08 9.60784 13.08 8.68Z"
fill="#0DBD8B"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.92 33.32C20.92 34.2478 20.1679 35 19.24 35C13.0544 35 8.04001 29.9856 8.04001 23.8C8.04001 22.8722 8.79217 22.12 9.72001 22.12C10.6478 22.12 11.4 22.8722 11.4 23.8C11.4 28.1299 14.9101 31.64 19.24 31.64C20.1679 31.64 20.92 32.3922 20.92 33.32Z"
fill="#0DBD8B"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.68 24.9199C3.75216 24.9199 3 24.1678 3 23.2399C3 17.0543 8.01441 12.0399 14.2 12.0399C15.1278 12.0399 15.88 12.7921 15.88 13.7199C15.88 14.6478 15.1278 15.3999 14.2 15.3999C9.87009 15.3999 6.36 18.91 6.36 23.2399C6.36 24.1678 5.60784 24.9199 4.68 24.9199Z"
fill="#0DBD8B"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M29.32 17.0801C30.2478 17.0801 31 17.8322 31 18.7601C31 24.9457 25.9856 29.9601 19.8 29.9601C18.8722 29.9601 18.12 29.2079 18.12 28.2801C18.12 27.3522 18.8722 26.6001 19.8 26.6001C24.1299 26.6001 27.64 23.09 27.64 18.7601C27.64 17.8322 28.3922 17.0801 29.32 17.0801Z"
fill="#0DBD8B"
/>
</svg>
</span>
<h1>Unable to load</h1>
</div>
<div class="mx_HomePage_col">
<div class="mx_HomePage_row">
<div>
<h2 id="step1_heading">Element can't load</h2>
<p>Something went wrong and Element was unable to load.</p>
</div>
</div> </div>
</div> </div>
<div class="mx_HomePage_row mx_Center mx_Spacer">
<p class="mx_Spacer">
<a href="https://element.io" target="_blank" class="mx_FooterLink"> Go to element.io </a>
</p>
</div>
</div> </div>
<div class="mx_HomePage_row mx_Center mx_Spacer">
<p class="mx_Spacer">
<a href="https://element.io" target="_blank" class="mx_FooterLink">
Go to element.io
</a>
</p>
</div>
</div>
</body> </body>

View File

@ -19,7 +19,7 @@ import { QueryDict, decodeParams } from "matrix-js-sdk/src/utils";
// We want to support some name / value pairs in the fragment // We want to support some name / value pairs in the fragment
// so we're re-using query string like format // so we're re-using query string like format
// //
export function parseQsFromFragment(location: Location): { location: string, params: QueryDict } { export function parseQsFromFragment(location: Location): { location: string; params: QueryDict } {
// if we have a fragment, it will start with '#', which we need to drop. // if we have a fragment, it will start with '#', which we need to drop.
// (if we don't, this will return ''). // (if we don't, this will return '').
const fragment = location.hash.substring(1); const fragment = location.hash.substring(1);
@ -27,7 +27,7 @@ export function parseQsFromFragment(location: Location): { location: string, par
// our fragment may contain a query-param-like section. we need to fish // our fragment may contain a query-param-like section. we need to fish
// this out *before* URI-decoding because the params may contain ? and & // this out *before* URI-decoding because the params may contain ? and &
// characters which are only URI-encoded once. // characters which are only URI-encoded once.
const hashparts = fragment.split('?'); const hashparts = fragment.split("?");
const result = { const result = {
location: decodeURIComponent(hashparts[0]), location: decodeURIComponent(hashparts[0]),

View File

@ -18,27 +18,27 @@ limitations under the License.
/* loading.js: test the myriad paths we have for loading the application */ /* loading.js: test the myriad paths we have for loading the application */
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import React from 'react'; import React from "react";
import { render, screen, fireEvent, waitFor, RenderResult, waitForElementToBeRemoved } from "@testing-library/react"; import { render, screen, fireEvent, waitFor, RenderResult, waitForElementToBeRemoved } from "@testing-library/react";
import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg'; import PlatformPeg from "matrix-react-sdk/src/PlatformPeg";
import { MatrixClientPeg } from 'matrix-react-sdk/src/MatrixClientPeg'; import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg";
import MatrixChat from 'matrix-react-sdk/src/components/structures/MatrixChat'; import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat";
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher'; import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
import MockHttpBackend from 'matrix-mock-request'; import MockHttpBackend from "matrix-mock-request";
import { makeType } from "matrix-react-sdk/src/utils/TypeUtils"; import { makeType } from "matrix-react-sdk/src/utils/TypeUtils";
import { ValidatedServerConfig } from 'matrix-react-sdk/src/utils/ValidatedServerConfig'; import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig";
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
import { QueryDict, sleep } from "matrix-js-sdk/src/utils"; import { QueryDict, sleep } from "matrix-js-sdk/src/utils";
import "../jest-mocks"; import "../jest-mocks";
import WebPlatform from '../../src/vector/platform/WebPlatform'; import WebPlatform from "../../src/vector/platform/WebPlatform";
import { parseQs, parseQsFromFragment } from '../../src/vector/url_utils'; import { parseQs, parseQsFromFragment } from "../../src/vector/url_utils";
import { cleanLocalstorage, deleteIndexedDB } from "../test-utils"; import { cleanLocalstorage, deleteIndexedDB } from "../test-utils";
const DEFAULT_HS_URL = 'http://my_server'; const DEFAULT_HS_URL = "http://my_server";
const DEFAULT_IS_URL = 'http://my_is'; const DEFAULT_IS_URL = "http://my_is";
describe('loading:', function() { describe("loading:", function () {
let parentDiv; let parentDiv;
let httpBackend; let httpBackend;
@ -51,10 +51,10 @@ describe('loading:', function() {
// a promise which resolves when the MatrixChat calls onTokenLoginCompleted // a promise which resolves when the MatrixChat calls onTokenLoginCompleted
let tokenLoginCompletePromise; let tokenLoginCompletePromise;
beforeEach(function() { beforeEach(function () {
httpBackend = new MockHttpBackend(); httpBackend = new MockHttpBackend();
window.fetch = httpBackend.fetchFn; window.fetch = httpBackend.fetchFn;
parentDiv = document.createElement('div'); parentDiv = document.createElement("div");
// uncomment this to actually add the div to the UI, to help with // uncomment this to actually add the div to the UI, to help with
// debugging (but slow things down) // debugging (but slow things down)
@ -64,17 +64,14 @@ describe('loading:', function() {
matrixChat = null; matrixChat = null;
}); });
afterEach(async function() { afterEach(async function () {
console.log(`${Date.now()}: loading: afterEach`); console.log(`${Date.now()}: loading: afterEach`);
matrixChat?.unmount(); matrixChat?.unmount();
// unmounting should have cleared the MatrixClientPeg // unmounting should have cleared the MatrixClientPeg
expect(MatrixClientPeg.get()).toBe(null); expect(MatrixClientPeg.get()).toBe(null);
// clear the indexeddbs so we can start from a clean slate next time. // clear the indexeddbs so we can start from a clean slate next time.
await Promise.all([ await Promise.all([deleteIndexedDB("matrix-js-sdk:crypto"), deleteIndexedDB("matrix-js-sdk:riot-web-sync")]);
deleteIndexedDB('matrix-js-sdk:crypto'),
deleteIndexedDB('matrix-js-sdk:riot-web-sync'),
]);
cleanLocalstorage(); cleanLocalstorage();
console.log(`${Date.now()}: loading: afterEach complete`); console.log(`${Date.now()}: loading: afterEach complete`);
}); });
@ -92,19 +89,21 @@ describe('loading:', function() {
windowLocation = { windowLocation = {
search: queryString, search: queryString,
hash: uriFragment, hash: uriFragment,
toString: function(): string { return this.search + this.hash; }, toString: function (): string {
return this.search + this.hash;
},
}; };
function onNewScreen(screen): void { function onNewScreen(screen): void {
console.log(Date.now() + " newscreen "+screen); console.log(Date.now() + " newscreen " + screen);
const hash = '#/' + screen; const hash = "#/" + screen;
windowLocation.hash = hash; windowLocation.hash = hash;
console.log(Date.now() + " browser URI now "+ windowLocation); console.log(Date.now() + " browser URI now " + windowLocation);
} }
// Parse the given window.location and return parameters that can be used when calling // Parse the given window.location and return parameters that can be used when calling
// MatrixChat.showScreen(screen, params) // MatrixChat.showScreen(screen, params)
function getScreenFromLocation(location): { screen: string, params: QueryDict } { function getScreenFromLocation(location): { screen: string; params: QueryDict } {
const fragparts = parseQsFromFragment(location); const fragparts = parseQsFromFragment(location);
return { return {
screen: fragparts.location.substring(1), screen: fragparts.location.substring(1),
@ -114,25 +113,28 @@ describe('loading:', function() {
const fragParts = parseQsFromFragment(windowLocation); const fragParts = parseQsFromFragment(windowLocation);
const config = Object.assign({ const config = Object.assign(
default_hs_url: DEFAULT_HS_URL, {
default_is_url: DEFAULT_IS_URL, default_hs_url: DEFAULT_HS_URL,
validated_server_config: makeType(ValidatedServerConfig, { default_is_url: DEFAULT_IS_URL,
hsUrl: DEFAULT_HS_URL, validated_server_config: makeType(ValidatedServerConfig, {
hsName: "TEST_ENVIRONMENT", hsUrl: DEFAULT_HS_URL,
hsNameIsDifferent: false, // yes, we lie hsName: "TEST_ENVIRONMENT",
isUrl: DEFAULT_IS_URL, hsNameIsDifferent: false, // yes, we lie
}), isUrl: DEFAULT_IS_URL,
embeddedPages: { }),
homeUrl: 'data:text/html;charset=utf-8;base64,PGh0bWw+PC9odG1sPg==', embeddedPages: {
homeUrl: "data:text/html;charset=utf-8;base64,PGh0bWw+PC9odG1sPg==",
},
}, },
}, opts.config || {}); opts.config || {},
);
PlatformPeg.set(new WebPlatform()); PlatformPeg.set(new WebPlatform());
const params = parseQs(windowLocation); const params = parseQs(windowLocation);
tokenLoginCompletePromise = new Promise<void>(resolve => { tokenLoginCompletePromise = new Promise<void>((resolve) => {
matrixChat = render( matrixChat = render(
<MatrixChat <MatrixChat
onNewScreen={onNewScreen} onNewScreen={onNewScreen}
@ -143,8 +145,11 @@ describe('loading:', function() {
enableGuest={true} enableGuest={true}
onTokenLoginCompleted={resolve} onTokenLoginCompleted={resolve}
initialScreenAfterLogin={getScreenFromLocation(windowLocation)} initialScreenAfterLogin={getScreenFromLocation(windowLocation)}
makeRegistrationUrl={(): string => {throw new Error('Not implemented');}} makeRegistrationUrl={(): string => {
/>, parentDiv, throw new Error("Not implemented");
}}
/>,
parentDiv,
); );
}); });
} }
@ -155,21 +160,23 @@ describe('loading:', function() {
// returns a promise resolving to the received request // returns a promise resolving to the received request
async function expectAndAwaitSync(opts?): Promise<any> { async function expectAndAwaitSync(opts?): Promise<any> {
let syncRequest = null; let syncRequest = null;
httpBackend.when('GET', '/_matrix/client/versions') httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
.respond(200, { versions: ["r0.3.0"],
"versions": ["r0.3.0"], unstable_features: {
"unstable_features": { "m.lazy_load_members": true,
"m.lazy_load_members": true, },
}, });
});
const isGuest = opts && opts.isGuest; const isGuest = opts && opts.isGuest;
if (!isGuest) { if (!isGuest) {
// the call to create the LL filter // the call to create the LL filter
httpBackend.when('POST', '/filter').respond(200, { filter_id: 'llfid' }); httpBackend.when("POST", "/filter").respond(200, { filter_id: "llfid" });
httpBackend.when('GET', '/pushrules').respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {});
} }
httpBackend.when('GET', '/sync') httpBackend
.check((r) => {syncRequest = r;}) .when("GET", "/sync")
.check((r) => {
syncRequest = r;
})
.respond(200, {}); .respond(200, {});
for (let attempts = 10; attempts > 0; attempts--) { for (let attempts = 10; attempts > 0; attempts--) {
@ -182,29 +189,35 @@ describe('loading:', function() {
throw new Error("Gave up waiting for /sync"); throw new Error("Gave up waiting for /sync");
} }
describe("Clean load with no stored credentials:", function() { describe("Clean load with no stored credentials:", function () {
it('gives a welcome page by default', function() { it("gives a welcome page by default", function () {
loadApp(); loadApp();
return sleep(1).then(async () => { return sleep(1)
// at this point, we're trying to do a guest registration; .then(async () => {
// we expect a spinner // at this point, we're trying to do a guest registration;
await assertAtLoadingSpinner(); // we expect a spinner
await assertAtLoadingSpinner();
httpBackend.when('POST', '/register').check(function(req) { httpBackend
expect(req.queryParams.kind).toEqual('guest'); .when("POST", "/register")
}).respond(403, "Guest access is disabled"); .check(function (req) {
expect(req.queryParams.kind).toEqual("guest");
})
.respond(403, "Guest access is disabled");
return httpBackend.flush(); return httpBackend.flush();
}).then(() => { })
// Wait for another trip around the event loop for the UI to update .then(() => {
return awaitWelcomeComponent(matrixChat); // Wait for another trip around the event loop for the UI to update
}).then(() => { return awaitWelcomeComponent(matrixChat);
return waitFor(() => expect(windowLocation.hash).toEqual("#/welcome")); })
}); .then(() => {
return waitFor(() => expect(windowLocation.hash).toEqual("#/welcome"));
});
}); });
it('should follow the original link after successful login', function() { it("should follow the original link after successful login", function () {
loadApp({ loadApp({
uriFragment: "#/room/!room:id", uriFragment: "#/room/!room:id",
}); });
@ -213,39 +226,48 @@ describe('loading:', function() {
httpBackend.when("GET", "/versions").respond(200, { versions: ["r0.4.0"] }); httpBackend.when("GET", "/versions").respond(200, { versions: ["r0.4.0"] });
httpBackend.when("GET", "/api/v1").respond(200, {}); httpBackend.when("GET", "/api/v1").respond(200, {});
return sleep(1).then(async () => { return sleep(1)
// at this point, we're trying to do a guest registration; .then(async () => {
// we expect a spinner // at this point, we're trying to do a guest registration;
await assertAtLoadingSpinner(); // we expect a spinner
await assertAtLoadingSpinner();
httpBackend.when('POST', '/register').check(function(req) { httpBackend
expect(req.queryParams.kind).toEqual('guest'); .when("POST", "/register")
}).respond(403, "Guest access is disabled"); .check(function (req) {
expect(req.queryParams.kind).toEqual("guest");
})
.respond(403, "Guest access is disabled");
return httpBackend.flush(); return httpBackend.flush();
}).then(() => { })
// Wait for another trip around the event loop for the UI to update .then(() => {
return sleep(10); // Wait for another trip around the event loop for the UI to update
}).then(() => { return sleep(10);
return moveFromWelcomeToLogin(matrixChat); })
}).then(() => { .then(() => {
return completeLogin(matrixChat); return moveFromWelcomeToLogin(matrixChat);
}).then(() => { })
// once the sync completes, we should have a room view .then(() => {
return awaitRoomView(matrixChat); return completeLogin(matrixChat);
}).then(() => { })
httpBackend.verifyNoOutstandingExpectation(); .then(() => {
expect(windowLocation.hash).toEqual("#/room/!room:id"); // once the sync completes, we should have a room view
return awaitRoomView(matrixChat);
})
.then(() => {
httpBackend.verifyNoOutstandingExpectation();
expect(windowLocation.hash).toEqual("#/room/!room:id");
// and the localstorage should have been updated // and the localstorage should have been updated
expect(localStorage.getItem('mx_user_id')).toEqual('@user:id'); expect(localStorage.getItem("mx_user_id")).toEqual("@user:id");
expect(localStorage.getItem('mx_access_token')).toEqual('access_token'); expect(localStorage.getItem("mx_access_token")).toEqual("access_token");
expect(localStorage.getItem('mx_hs_url')).toEqual(DEFAULT_HS_URL); expect(localStorage.getItem("mx_hs_url")).toEqual(DEFAULT_HS_URL);
expect(localStorage.getItem('mx_is_url')).toEqual(DEFAULT_IS_URL); expect(localStorage.getItem("mx_is_url")).toEqual(DEFAULT_IS_URL);
}); });
}); });
it.skip('should not register as a guest when using a #/login link', function() { it.skip("should not register as a guest when using a #/login link", function () {
loadApp({ loadApp({
uriFragment: "#/login", uriFragment: "#/login",
}); });
@ -254,37 +276,35 @@ describe('loading:', function() {
httpBackend.when("GET", "/versions").respond(200, { versions: ["r0.4.0"] }); httpBackend.when("GET", "/versions").respond(200, { versions: ["r0.4.0"] });
httpBackend.when("GET", "/api/v1").respond(200, {}); httpBackend.when("GET", "/api/v1").respond(200, {});
return awaitLoginComponent(matrixChat).then(async () => { return awaitLoginComponent(matrixChat)
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading...")); .then(async () => {
// we expect a single <Login> component await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading..."));
await screen.findByRole("main"); // we expect a single <Login> component
screen.getAllByText("Sign in"); await screen.findByRole("main");
screen.getAllByText("Sign in");
// the only outstanding request should be a GET /login // the only outstanding request should be a GET /login
// (in particular there should be no /register request for // (in particular there should be no /register request for
// guest registration). // guest registration).
const allowedRequests = [ const allowedRequests = ["/_matrix/client/r0/login", "/versions", "/api/v1"];
"/_matrix/client/r0/login", for (const req of httpBackend.requests) {
"/versions", if (req.method === "GET" && allowedRequests.find((p) => req.path.endsWith(p))) {
"/api/v1", continue;
]; }
for (const req of httpBackend.requests) {
if (req.method === 'GET' && allowedRequests.find(p => req.path.endsWith(p))) { throw new Error(`Unexpected HTTP request to ${req}`);
continue;
} }
return completeLogin(matrixChat);
throw new Error(`Unexpected HTTP request to ${req}`); })
} .then(() => {
return completeLogin(matrixChat); expect(matrixChat.container.querySelector(".mx_HomePage")).toBeTruthy();
}).then(() => { expect(windowLocation.hash).toEqual("#/home");
expect(matrixChat.container.querySelector(".mx_HomePage")).toBeTruthy(); });
expect(windowLocation.hash).toEqual("#/home");
});
}); });
}); });
describe("MatrixClient rehydrated from stored credentials:", function() { describe("MatrixClient rehydrated from stored credentials:", function () {
beforeEach(async function() { beforeEach(async function () {
localStorage.setItem("mx_hs_url", "http://localhost"); localStorage.setItem("mx_hs_url", "http://localhost");
localStorage.setItem("mx_is_url", "http://localhost"); localStorage.setItem("mx_is_url", "http://localhost");
localStorage.setItem("mx_access_token", "access_token"); localStorage.setItem("mx_access_token", "access_token");
@ -292,63 +312,68 @@ describe('loading:', function() {
localStorage.setItem("mx_last_room_id", "!last_room:id"); localStorage.setItem("mx_last_room_id", "!last_room:id");
// Create a crypto store as well to satisfy storage consistency checks // Create a crypto store as well to satisfy storage consistency checks
const cryptoStore = new IndexedDBCryptoStore( const cryptoStore = new IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto");
indexedDB,
"matrix-js-sdk:crypto",
);
await cryptoStore.startup(); await cryptoStore.startup();
}); });
it('shows the last known room by default', function() { it("shows the last known room by default", function () {
loadApp(); loadApp();
return awaitLoggedIn(matrixChat).then(() => { return awaitLoggedIn(matrixChat)
// we are logged in - let the sync complete .then(() => {
return expectAndAwaitSync(); // we are logged in - let the sync complete
}).then(() => { return expectAndAwaitSync();
// once the sync completes, we should have a room view })
return awaitRoomView(matrixChat); .then(() => {
}).then(() => { // once the sync completes, we should have a room view
httpBackend.verifyNoOutstandingExpectation(); return awaitRoomView(matrixChat);
expect(windowLocation.hash).toEqual("#/room/!last_room:id"); })
}); .then(() => {
httpBackend.verifyNoOutstandingExpectation();
expect(windowLocation.hash).toEqual("#/room/!last_room:id");
});
}); });
it('shows a home page by default if we have no joined rooms', function() { it("shows a home page by default if we have no joined rooms", function () {
localStorage.removeItem("mx_last_room_id"); localStorage.removeItem("mx_last_room_id");
loadApp(); loadApp();
return awaitLoggedIn(matrixChat).then(() => { return awaitLoggedIn(matrixChat)
// we are logged in - let the sync complete .then(() => {
return expectAndAwaitSync(); // we are logged in - let the sync complete
}).then(() => { return expectAndAwaitSync();
// once the sync completes, we should have a home page })
httpBackend.verifyNoOutstandingExpectation(); .then(() => {
expect(matrixChat.container.querySelector(".mx_HomePage")).toBeTruthy(); // once the sync completes, we should have a home page
expect(windowLocation.hash).toEqual("#/home"); httpBackend.verifyNoOutstandingExpectation();
}); expect(matrixChat.container.querySelector(".mx_HomePage")).toBeTruthy();
expect(windowLocation.hash).toEqual("#/home");
});
}); });
it('shows a room view if we followed a room link', function() { it("shows a room view if we followed a room link", function () {
loadApp({ loadApp({
uriFragment: "#/room/!room:id", uriFragment: "#/room/!room:id",
}); });
return awaitLoggedIn(matrixChat).then(() => { return awaitLoggedIn(matrixChat)
// we are logged in - let the sync complete .then(() => {
return expectAndAwaitSync(); // we are logged in - let the sync complete
}).then(() => { return expectAndAwaitSync();
// once the sync completes, we should have a room view })
return awaitRoomView(matrixChat); .then(() => {
}).then(() => { // once the sync completes, we should have a room view
httpBackend.verifyNoOutstandingExpectation(); return awaitRoomView(matrixChat);
expect(windowLocation.hash).toEqual("#/room/!room:id"); })
}); .then(() => {
httpBackend.verifyNoOutstandingExpectation();
expect(windowLocation.hash).toEqual("#/room/!room:id");
});
}); });
describe('/#/login link:', function() { describe("/#/login link:", function () {
beforeEach(function() { beforeEach(function () {
loadApp({ loadApp({
uriFragment: "#/login", uriFragment: "#/login",
}); });
@ -357,7 +382,7 @@ describe('loading:', function() {
return expectAndAwaitSync(); return expectAndAwaitSync();
}); });
it('does not show a login view', async function() { it("does not show a login view", async function () {
await awaitRoomView(matrixChat); await awaitRoomView(matrixChat);
await screen.findByLabelText("Spaces"); await screen.findByLabelText("Spaces");
@ -366,136 +391,165 @@ describe('loading:', function() {
}); });
}); });
describe('Guest auto-registration:', function() { describe("Guest auto-registration:", function () {
it('shows a welcome page by default', function() { it("shows a welcome page by default", function () {
loadApp(); loadApp();
return sleep(1).then(async () => { return sleep(1)
// at this point, we're trying to do a guest registration; .then(async () => {
// we expect a spinner // at this point, we're trying to do a guest registration;
await assertAtLoadingSpinner(); // we expect a spinner
await assertAtLoadingSpinner();
httpBackend.when('POST', '/register').check(function(req) { httpBackend
expect(req.queryParams.kind).toEqual('guest'); .when("POST", "/register")
}).respond(200, { .check(function (req) {
user_id: "@guest:localhost", expect(req.queryParams.kind).toEqual("guest");
access_token: "secret_token", })
.respond(200, {
user_id: "@guest:localhost",
access_token: "secret_token",
});
return httpBackend.flush();
})
.then(() => {
return awaitLoggedIn(matrixChat);
})
.then(() => {
// we are logged in - let the sync complete
return expectAndAwaitSync({ isGuest: true });
})
.then(() => {
// once the sync completes, we should have a welcome page
httpBackend.verifyNoOutstandingExpectation();
expect(matrixChat.container.querySelector(".mx_Welcome")).toBeTruthy();
expect(windowLocation.hash).toEqual("#/welcome");
}); });
return httpBackend.flush();
}).then(() => {
return awaitLoggedIn(matrixChat);
}).then(() => {
// we are logged in - let the sync complete
return expectAndAwaitSync({ isGuest: true });
}).then(() => {
// once the sync completes, we should have a welcome page
httpBackend.verifyNoOutstandingExpectation();
expect(matrixChat.container.querySelector(".mx_Welcome")).toBeTruthy();
expect(windowLocation.hash).toEqual("#/welcome");
});
}); });
it('uses the default homeserver to register with', function() { it("uses the default homeserver to register with", function () {
loadApp(); loadApp();
return sleep(1).then(async () => { return sleep(1)
// at this point, we're trying to do a guest registration; .then(async () => {
// we expect a spinner // at this point, we're trying to do a guest registration;
await assertAtLoadingSpinner(); // we expect a spinner
await assertAtLoadingSpinner();
httpBackend.when('POST', '/register').check(function(req) { httpBackend
.when("POST", "/register")
.check(function (req) {
expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true);
expect(req.queryParams.kind).toEqual("guest");
})
.respond(200, {
user_id: "@guest:localhost",
access_token: "secret_token",
});
return httpBackend.flush();
})
.then(() => {
return awaitLoggedIn(matrixChat);
})
.then(() => {
return expectAndAwaitSync({ isGuest: true });
})
.then((req) => {
expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true); expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true);
expect(req.queryParams.kind).toEqual('guest');
}).respond(200, { // once the sync completes, we should have a welcome page
user_id: "@guest:localhost", httpBackend.verifyNoOutstandingExpectation();
access_token: "secret_token", expect(matrixChat.container.querySelector(".mx_Welcome")).toBeTruthy();
expect(windowLocation.hash).toEqual("#/welcome");
expect(MatrixClientPeg.get().baseUrl).toEqual(DEFAULT_HS_URL);
expect(MatrixClientPeg.get().idBaseUrl).toEqual(DEFAULT_IS_URL);
}); });
return httpBackend.flush();
}).then(() => {
return awaitLoggedIn(matrixChat);
}).then(() => {
return expectAndAwaitSync({ isGuest: true });
}).then((req) => {
expect(req.path.startsWith(DEFAULT_HS_URL)).toBe(true);
// once the sync completes, we should have a welcome page
httpBackend.verifyNoOutstandingExpectation();
expect(matrixChat.container.querySelector(".mx_Welcome")).toBeTruthy();
expect(windowLocation.hash).toEqual("#/welcome");
expect(MatrixClientPeg.get().baseUrl).toEqual(DEFAULT_HS_URL);
expect(MatrixClientPeg.get().idBaseUrl).toEqual(DEFAULT_IS_URL);
});
}); });
it('shows a room view if we followed a room link', function() { it("shows a room view if we followed a room link", function () {
loadApp({ loadApp({
uriFragment: "#/room/!room:id", uriFragment: "#/room/!room:id",
}); });
return sleep(1).then(async () => { return sleep(1)
// at this point, we're trying to do a guest registration; .then(async () => {
// we expect a spinner // at this point, we're trying to do a guest registration;
await assertAtLoadingSpinner(); // we expect a spinner
await assertAtLoadingSpinner();
httpBackend.when('POST', '/register').check(function(req) { httpBackend
expect(req.queryParams.kind).toEqual('guest'); .when("POST", "/register")
}).respond(200, { .check(function (req) {
user_id: "@guest:localhost", expect(req.queryParams.kind).toEqual("guest");
access_token: "secret_token", })
.respond(200, {
user_id: "@guest:localhost",
access_token: "secret_token",
});
return httpBackend.flush();
})
.then(() => {
return awaitLoggedIn(matrixChat);
})
.then(() => {
return expectAndAwaitSync({ isGuest: true });
})
.then(() => {
// once the sync completes, we should have a room view
return awaitRoomView(matrixChat);
})
.then(() => {
httpBackend.verifyNoOutstandingExpectation();
expect(windowLocation.hash).toEqual("#/room/!room:id");
}); });
return httpBackend.flush();
}).then(() => {
return awaitLoggedIn(matrixChat);
}).then(() => {
return expectAndAwaitSync({ isGuest: true });
}).then(() => {
// once the sync completes, we should have a room view
return awaitRoomView(matrixChat);
}).then(() => {
httpBackend.verifyNoOutstandingExpectation();
expect(windowLocation.hash).toEqual("#/room/!room:id");
});
}); });
describe('Login as user', function() { describe("Login as user", function () {
beforeEach(function() { beforeEach(function () {
// first we have to load the homepage // first we have to load the homepage
loadApp(); loadApp();
httpBackend.when('POST', '/register').check(function(req) { httpBackend
expect(req.queryParams.kind).toEqual('guest'); .when("POST", "/register")
}).respond(200, { .check(function (req) {
user_id: "@guest:localhost", expect(req.queryParams.kind).toEqual("guest");
access_token: "secret_token", })
}); .respond(200, {
user_id: "@guest:localhost",
access_token: "secret_token",
});
return httpBackend.flush().then(() => { return httpBackend
return awaitLoggedIn(matrixChat); .flush()
}).then(() => { .then(() => {
// we got a sync spinner - let the sync complete return awaitLoggedIn(matrixChat);
return expectAndAwaitSync(); })
}).then(async () => { .then(() => {
// once the sync completes, we should have a home page // we got a sync spinner - let the sync complete
await waitFor(() => matrixChat.container.querySelector(".mx_HomePage")); return expectAndAwaitSync();
})
.then(async () => {
// once the sync completes, we should have a home page
await waitFor(() => matrixChat.container.querySelector(".mx_HomePage"));
// we simulate a click on the 'login' button by firing off // we simulate a click on the 'login' button by firing off
// the relevant dispatch. // the relevant dispatch.
// //
// XXX: is it an anti-pattern to access the react-sdk's // XXX: is it an anti-pattern to access the react-sdk's
// dispatcher in this way? Is it better to find the login // dispatcher in this way? Is it better to find the login
// button and simulate a click? (we might have to arrange // button and simulate a click? (we might have to arrange
// for it to be shown - it's not always, due to the // for it to be shown - it's not always, due to the
// collapsing left panel // collapsing left panel
dis.dispatch({ action: 'start_login' }); dis.dispatch({ action: "start_login" });
return awaitLoginComponent(matrixChat); return awaitLoginComponent(matrixChat);
}); });
}); });
it('should give us a login page', async function() { it("should give us a login page", async function () {
// we expect a single <Login> component // we expect a single <Login> component
await screen.findByRole("main"); await screen.findByRole("main");
screen.getAllByText("Sign in"); screen.getAllByText("Sign in");
@ -505,44 +559,50 @@ describe('loading:', function() {
}); });
}); });
describe('Token login:', function() { describe("Token login:", function () {
it('logs in successfully', function() { it("logs in successfully", function () {
localStorage.setItem("mx_sso_hs_url", "https://homeserver"); localStorage.setItem("mx_sso_hs_url", "https://homeserver");
localStorage.setItem("mx_sso_is_url", "https://idserver"); localStorage.setItem("mx_sso_is_url", "https://idserver");
loadApp({ loadApp({
queryString: "?loginToken=secretToken", queryString: "?loginToken=secretToken",
}); });
return sleep(1).then(async () => { return sleep(1)
// we expect a spinner while we're logging in .then(async () => {
await assertAtLoadingSpinner(); // we expect a spinner while we're logging in
await assertAtLoadingSpinner();
httpBackend.when('POST', '/login').check(function(req) { httpBackend
expect(req.path).toMatch(new RegExp("^https://homeserver/")); .when("POST", "/login")
expect(req.data.type).toEqual("m.login.token"); .check(function (req) {
expect(req.data.token).toEqual("secretToken"); expect(req.path).toMatch(new RegExp("^https://homeserver/"));
}).respond(200, { expect(req.data.type).toEqual("m.login.token");
user_id: "@user:localhost", expect(req.data.token).toEqual("secretToken");
device_id: 'DEVICE_ID', })
access_token: "access_token", .respond(200, {
user_id: "@user:localhost",
device_id: "DEVICE_ID",
access_token: "access_token",
});
return httpBackend.flush();
})
.then(() => {
// at this point, MatrixChat should fire onTokenLoginCompleted, which
// makes index.js reload the app. We're not going to attempt to
// simulate the reload - just check that things are left in the
// right state for the reloaded app.
return tokenLoginCompletePromise;
})
.then(() => {
// check that the localstorage has been set up in such a way that
// the reloaded app can pick up where we leave off.
expect(localStorage.getItem("mx_user_id")).toEqual("@user:localhost");
expect(localStorage.getItem("mx_access_token")).toEqual("access_token");
expect(localStorage.getItem("mx_hs_url")).toEqual("https://homeserver");
expect(localStorage.getItem("mx_is_url")).toEqual("https://idserver");
}); });
return httpBackend.flush();
}).then(() => {
// at this point, MatrixChat should fire onTokenLoginCompleted, which
// makes index.js reload the app. We're not going to attempt to
// simulate the reload - just check that things are left in the
// right state for the reloaded app.
return tokenLoginCompletePromise;
}).then(() => {
// check that the localstorage has been set up in such a way that
// the reloaded app can pick up where we leave off.
expect(localStorage.getItem('mx_user_id')).toEqual('@user:localhost');
expect(localStorage.getItem('mx_access_token')).toEqual('access_token');
expect(localStorage.getItem('mx_hs_url')).toEqual('https://homeserver');
expect(localStorage.getItem('mx_is_url')).toEqual('https://idserver');
});
}); });
}); });
@ -551,38 +611,44 @@ describe('loading:', function() {
async function completeLogin(matrixChat: RenderResult): Promise<void> { async function completeLogin(matrixChat: RenderResult): Promise<void> {
// When we switch to the login component, it'll hit the login endpoint // When we switch to the login component, it'll hit the login endpoint
// for proof of life and to get flows. We'll only give it one option. // for proof of life and to get flows. We'll only give it one option.
httpBackend.when('GET', '/login') httpBackend.when("GET", "/login").respond(200, { flows: [{ type: "m.login.password" }] });
.respond(200, { flows: [{ type: "m.login.password" }] });
httpBackend.flush(); // We already would have tried the GET /login request httpBackend.flush(); // We already would have tried the GET /login request
// Give the component some time to finish processing the login flows before // Give the component some time to finish processing the login flows before
// continuing. // continuing.
await sleep(100); await sleep(100);
httpBackend.when('POST', '/login').check(function(req) { httpBackend
expect(req.data.type).toEqual('m.login.password'); .when("POST", "/login")
expect(req.data.identifier.type).toEqual('m.id.user'); .check(function (req) {
expect(req.data.identifier.user).toEqual('user'); expect(req.data.type).toEqual("m.login.password");
expect(req.data.password).toEqual('pass'); expect(req.data.identifier.type).toEqual("m.id.user");
}).respond(200, { expect(req.data.identifier.user).toEqual("user");
user_id: '@user:id', expect(req.data.password).toEqual("pass");
device_id: 'DEVICE_ID', })
access_token: 'access_token', .respond(200, {
}); user_id: "@user:id",
device_id: "DEVICE_ID",
access_token: "access_token",
});
fireEvent.change(matrixChat.container.querySelector("#mx_LoginForm_username"), { target: { value: "user" } }); fireEvent.change(matrixChat.container.querySelector("#mx_LoginForm_username"), { target: { value: "user" } });
fireEvent.change(matrixChat.container.querySelector("#mx_LoginForm_password"), { target: { value: "pass" } }); fireEvent.change(matrixChat.container.querySelector("#mx_LoginForm_password"), { target: { value: "pass" } });
fireEvent.click(screen.getByText("Sign in", { selector: ".mx_Login_submit" })); fireEvent.click(screen.getByText("Sign in", { selector: ".mx_Login_submit" }));
return httpBackend.flush().then(() => { return httpBackend
// Wait for another trip around the event loop for the UI to update .flush()
return sleep(1); .then(() => {
}).then(() => { // Wait for another trip around the event loop for the UI to update
return expectAndAwaitSync().catch((e) => { return sleep(1);
throw new Error("Never got /sync after login: did the client start?"); })
.then(() => {
return expectAndAwaitSync().catch((e) => {
throw new Error("Never got /sync after login: did the client start?");
});
})
.then(() => {
httpBackend.verifyNoOutstandingExpectation();
}); });
}).then(() => {
httpBackend.verifyNoOutstandingExpectation();
});
} }
}); });
@ -594,7 +660,7 @@ async function assertAtLoadingSpinner(): Promise<void> {
async function awaitLoggedIn(matrixChat: RenderResult): Promise<void> { async function awaitLoggedIn(matrixChat: RenderResult): Promise<void> {
if (matrixChat.container.querySelector(".mx_MatrixChat_wrapper")) return; // already logged in if (matrixChat.container.querySelector(".mx_MatrixChat_wrapper")) return; // already logged in
return new Promise(resolve => { return new Promise((resolve) => {
const onAction = ({ action }): void => { const onAction = ({ action }): void => {
if (action !== "on_logged_in") { if (action !== "on_logged_in") {
return; return;
@ -621,6 +687,6 @@ async function awaitWelcomeComponent(matrixChat: RenderResult): Promise<void> {
} }
function moveFromWelcomeToLogin(matrixChat: RenderResult): Promise<void> { function moveFromWelcomeToLogin(matrixChat: RenderResult): Promise<void> {
dis.dispatch({ action: 'start_login' }); dis.dispatch({ action: "start_login" });
return awaitLoginComponent(matrixChat); return awaitLoginComponent(matrixChat);
} }

View File

@ -15,9 +15,9 @@ limitations under the License.
*/ */
// https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom // https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, "matchMedia", {
writable: true, writable: true,
value: jest.fn().mockImplementation(query => ({ value: jest.fn().mockImplementation((query) => ({
matches: false, matches: false,
media: query, media: query,
onchange: null, onchange: null,

View File

@ -34,14 +34,12 @@ export function deleteIndexedDB(dbName: string): Promise<void> {
}; };
req.onerror = (ev): void => { req.onerror = (ev): void => {
reject(new Error( reject(new Error(`${Date.now()}: unable to delete indexeddb ${dbName}: ${req.error}`));
`${Date.now()}: unable to delete indexeddb ${dbName}: ${req.error}`,
));
}; };
req.onsuccess = (): void => { req.onsuccess = (): void => {
const now = Date.now(); const now = Date.now();
console.log(`${now}: Removed indexeddb instance: ${dbName} in ${now-startTime} ms`); console.log(`${now}: Removed indexeddb instance: ${dbName} in ${now - startTime} ms`);
resolve(); resolve();
}; };
}).catch((e) => { }).catch((e) => {

View File

@ -20,88 +20,88 @@ import { getVectorConfig } from "../../../src/vector/getconfig";
fetchMock.config.overwriteRoutes = true; fetchMock.config.overwriteRoutes = true;
describe('getVectorConfig()', () => { describe("getVectorConfig()", () => {
const prevDocumentDomain = document.domain; const prevDocumentDomain = document.domain;
const elementDomain = 'app.element.io'; const elementDomain = "app.element.io";
const now = 1234567890; const now = 1234567890;
const specificConfig = { const specificConfig = {
brand: 'specific', brand: "specific",
}; };
const generalConfig = { const generalConfig = {
brand: 'general', brand: "general",
}; };
beforeEach(() => { beforeEach(() => {
document.domain = elementDomain; document.domain = elementDomain;
// stable value for cachebuster // stable value for cachebuster
jest.spyOn(Date, 'now').mockReturnValue(now); jest.spyOn(Date, "now").mockReturnValue(now);
jest.clearAllMocks(); jest.clearAllMocks();
fetchMock.mockClear(); fetchMock.mockClear();
}); });
afterAll(() => { afterAll(() => {
document.domain = prevDocumentDomain; document.domain = prevDocumentDomain;
jest.spyOn(Date, 'now').mockRestore(); jest.spyOn(Date, "now").mockRestore();
}); });
it('requests specific config for document domain', async () => { it("requests specific config for document domain", async () => {
fetchMock.getOnce("express:/config.app.element.io.json", specificConfig); fetchMock.getOnce("express:/config.app.element.io.json", specificConfig);
fetchMock.getOnce("express:/config.json", generalConfig); fetchMock.getOnce("express:/config.json", generalConfig);
await expect(getVectorConfig()).resolves.toEqual(specificConfig); await expect(getVectorConfig()).resolves.toEqual(specificConfig);
}); });
it('adds trailing slash to relativeLocation when not an empty string', async () => { it("adds trailing slash to relativeLocation when not an empty string", async () => {
fetchMock.getOnce("express:../config.app.element.io.json", specificConfig); fetchMock.getOnce("express:../config.app.element.io.json", specificConfig);
fetchMock.getOnce("express:../config.json", generalConfig); fetchMock.getOnce("express:../config.json", generalConfig);
await expect(getVectorConfig("..")).resolves.toEqual(specificConfig); await expect(getVectorConfig("..")).resolves.toEqual(specificConfig);
}); });
it('returns general config when specific config succeeds but is empty', async () => { it("returns general config when specific config succeeds but is empty", async () => {
fetchMock.getOnce("express:/config.app.element.io.json", {}); fetchMock.getOnce("express:/config.app.element.io.json", {});
fetchMock.getOnce("express:/config.json", generalConfig); fetchMock.getOnce("express:/config.json", generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig); await expect(getVectorConfig()).resolves.toEqual(generalConfig);
}); });
it('returns general config when specific config 404s', async () => { it("returns general config when specific config 404s", async () => {
fetchMock.getOnce("express:/config.app.element.io.json", { status: 404 }); fetchMock.getOnce("express:/config.app.element.io.json", { status: 404 });
fetchMock.getOnce("express:/config.json", generalConfig); fetchMock.getOnce("express:/config.json", generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig); await expect(getVectorConfig()).resolves.toEqual(generalConfig);
}); });
it('returns general config when specific config is fetched from a file and is empty', async () => { it("returns general config when specific config is fetched from a file and is empty", async () => {
fetchMock.getOnce("express:/config.app.element.io.json", 0); fetchMock.getOnce("express:/config.app.element.io.json", 0);
fetchMock.getOnce("express:/config.json", generalConfig); fetchMock.getOnce("express:/config.json", generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig); await expect(getVectorConfig()).resolves.toEqual(generalConfig);
}); });
it('returns general config when specific config returns a non-200 status', async () => { it("returns general config when specific config returns a non-200 status", async () => {
fetchMock.getOnce("express:/config.app.element.io.json", { status: 401 }); fetchMock.getOnce("express:/config.app.element.io.json", { status: 401 });
fetchMock.getOnce("express:/config.json", generalConfig); fetchMock.getOnce("express:/config.json", generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig); await expect(getVectorConfig()).resolves.toEqual(generalConfig);
}); });
it('returns general config when specific config returns an error', async () => { it("returns general config when specific config returns an error", async () => {
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err1" }); fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err1" });
fetchMock.getOnce("express:/config.json", generalConfig); fetchMock.getOnce("express:/config.json", generalConfig);
await expect(getVectorConfig()).resolves.toEqual(generalConfig); await expect(getVectorConfig()).resolves.toEqual(generalConfig);
}); });
it('rejects with an error when general config rejects', async () => { it("rejects with an error when general config rejects", async () => {
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" }); fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" });
fetchMock.getOnce("express:/config.json", { throws: "err-general" }); fetchMock.getOnce("express:/config.json", { throws: "err-general" });
await expect(getVectorConfig()).rejects.toBe("err-general"); await expect(getVectorConfig()).rejects.toBe("err-general");
}); });
it('rejects with an error when config is invalid JSON', async () => { it("rejects with an error when config is invalid JSON", async () => {
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" }); fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" });
fetchMock.getOnce("express:/config.json", '{"invalid": "json",}'); fetchMock.getOnce("express:/config.json", '{"invalid": "json",}');

View File

@ -14,33 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { logger } from 'matrix-js-sdk/src/logger'; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent, Room } from 'matrix-js-sdk/src/matrix'; import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { UpdateCheckStatus } from 'matrix-react-sdk/src/BasePlatform'; import { UpdateCheckStatus } from "matrix-react-sdk/src/BasePlatform";
import { Action } from 'matrix-react-sdk/src/dispatcher/actions'; import { Action } from "matrix-react-sdk/src/dispatcher/actions";
import dispatcher from 'matrix-react-sdk/src/dispatcher/dispatcher'; import dispatcher from "matrix-react-sdk/src/dispatcher/dispatcher";
import * as rageshake from 'matrix-react-sdk/src/rageshake/rageshake'; import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
import ElectronPlatform from '../../../../src/vector/platform/ElectronPlatform'; import ElectronPlatform from "../../../../src/vector/platform/ElectronPlatform";
jest.mock('matrix-react-sdk/src/rageshake/rageshake', () => ({ jest.mock("matrix-react-sdk/src/rageshake/rageshake", () => ({
flush: jest.fn(), flush: jest.fn(),
})); }));
describe('ElectronPlatform', () => { describe("ElectronPlatform", () => {
const defaultUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' + const defaultUserAgent =
'(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36'; "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36";
const mockElectron = { const mockElectron = {
on: jest.fn(), on: jest.fn(),
send: jest.fn(), send: jest.fn(),
}; };
const dispatchSpy = jest.spyOn(dispatcher, 'dispatch'); const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
const dispatchFireSpy = jest.spyOn(dispatcher, 'fire'); const dispatchFireSpy = jest.spyOn(dispatcher, "fire");
const logSpy = jest.spyOn(logger, 'log').mockImplementation(() => {}); const logSpy = jest.spyOn(logger, "log").mockImplementation(() => {});
const userId = '@alice:server.org'; const userId = "@alice:server.org";
const deviceId = 'device-id'; const deviceId = "device-id";
window.electron = mockElectron; window.electron = mockElectron;
beforeEach(() => { beforeEach(() => {
@ -53,9 +54,9 @@ describe('ElectronPlatform', () => {
const getElectronEventHandlerCall = (eventType: string): [type: string, handler: Function] | undefined => const getElectronEventHandlerCall = (eventType: string): [type: string, handler: Function] | undefined =>
mockElectron.on.mock.calls.find(([type]) => type === eventType); mockElectron.on.mock.calls.find(([type]) => type === eventType);
it('flushes rageshake before quitting', () => { it("flushes rageshake before quitting", () => {
new ElectronPlatform(); new ElectronPlatform();
const [event, handler] = getElectronEventHandlerCall('before-quit'); const [event, handler] = getElectronEventHandlerCall("before-quit");
// correct event bound // correct event bound
expect(event).toBeTruthy(); expect(event).toBeTruthy();
@ -65,9 +66,9 @@ describe('ElectronPlatform', () => {
expect(rageshake.flush).toHaveBeenCalled(); expect(rageshake.flush).toHaveBeenCalled();
}); });
it('dispatches view settings action on preferences event', () => { it("dispatches view settings action on preferences event", () => {
new ElectronPlatform(); new ElectronPlatform();
const [event, handler] = getElectronEventHandlerCall('preferences'); const [event, handler] = getElectronEventHandlerCall("preferences");
// correct event bound // correct event bound
expect(event).toBeTruthy(); expect(event).toBeTruthy();
@ -76,10 +77,10 @@ describe('ElectronPlatform', () => {
expect(dispatchFireSpy).toHaveBeenCalledWith(Action.ViewUserSettings); expect(dispatchFireSpy).toHaveBeenCalledWith(Action.ViewUserSettings);
}); });
describe('updates', () => { describe("updates", () => {
it('dispatches on check updates action', () => { it("dispatches on check updates action", () => {
new ElectronPlatform(); new ElectronPlatform();
const [event, handler] = getElectronEventHandlerCall('check_updates'); const [event, handler] = getElectronEventHandlerCall("check_updates");
// correct event bound // correct event bound
expect(event).toBeTruthy(); expect(event).toBeTruthy();
@ -90,9 +91,9 @@ describe('ElectronPlatform', () => {
}); });
}); });
it('dispatches on check updates action when update not available', () => { it("dispatches on check updates action when update not available", () => {
new ElectronPlatform(); new ElectronPlatform();
const [, handler] = getElectronEventHandlerCall('check_updates'); const [, handler] = getElectronEventHandlerCall("check_updates");
handler({}, false); handler({}, false);
expect(dispatchSpy).toHaveBeenCalledWith({ expect(dispatchSpy).toHaveBeenCalledWith({
@ -101,55 +102,42 @@ describe('ElectronPlatform', () => {
}); });
}); });
it('starts update check', () => { it("starts update check", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
platform.startUpdateCheck(); platform.startUpdateCheck();
expect(mockElectron.send).toHaveBeenCalledWith('check_updates'); expect(mockElectron.send).toHaveBeenCalledWith("check_updates");
}); });
it('installs update', () => { it("installs update", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
platform.installUpdate(); platform.installUpdate();
expect(mockElectron.send).toHaveBeenCalledWith('install_update'); expect(mockElectron.send).toHaveBeenCalledWith("install_update");
}); });
}); });
it('returns human readable name', () => { it("returns human readable name", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.getHumanReadableName()).toEqual('Electron Platform'); expect(platform.getHumanReadableName()).toEqual("Electron Platform");
}); });
describe("getDefaultDeviceDisplayName", () => { describe("getDefaultDeviceDisplayName", () => {
it.each([[ it.each([
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + [
"(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
"Element Desktop: macOS", "(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
], "Element Desktop: macOS",
[ ],
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) " + [
"electron/1.0.0 Chrome/53.0.2785.113 Electron/1.4.3 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Element Desktop: Windows", "electron/1.0.0 Chrome/53.0.2785.113 Electron/1.4.3 Safari/537.36",
], "Element Desktop: Windows",
[ ],
"Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0", ["Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: Linux"],
"Element Desktop: Linux", ["Mozilla/5.0 (X11; FreeBSD i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: FreeBSD"],
], ["Mozilla/5.0 (X11; OpenBSD i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: OpenBSD"],
[ ["Mozilla/5.0 (X11; SunOS i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Element Desktop: SunOS"],
"Mozilla/5.0 (X11; FreeBSD i686; rv:21.0) Gecko/20100101 Firefox/21.0", ["custom user agent", "Element Desktop: Unknown"],
"Element Desktop: FreeBSD", ])("%s = %s", (userAgent, result) => {
],
[
"Mozilla/5.0 (X11; OpenBSD i686; rv:21.0) Gecko/20100101 Firefox/21.0",
"Element Desktop: OpenBSD",
],
[
"Mozilla/5.0 (X11; SunOS i686; rv:21.0) Gecko/20100101 Firefox/21.0",
"Element Desktop: SunOS",
],
[
"custom user agent",
"Element Desktop: Unknown",
]])("%s = %s", (userAgent, result) => {
delete window.navigator; delete window.navigator;
window.navigator = { userAgent } as unknown as Navigator; window.navigator = { userAgent } as unknown as Navigator;
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
@ -157,119 +145,119 @@ describe('ElectronPlatform', () => {
}); });
}); });
it('returns true for needsUrlTooltips', () => { it("returns true for needsUrlTooltips", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.needsUrlTooltips()).toBe(true); expect(platform.needsUrlTooltips()).toBe(true);
}); });
it('should override browser shortcuts', () => { it("should override browser shortcuts", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.overrideBrowserShortcuts()).toBe(true); expect(platform.overrideBrowserShortcuts()).toBe(true);
}); });
it('allows overriding native context menus', () => { it("allows overriding native context menus", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.allowOverridingNativeContextMenus()).toBe(true); expect(platform.allowOverridingNativeContextMenus()).toBe(true);
}); });
it('indicates support for desktop capturer', () => { it("indicates support for desktop capturer", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.supportsDesktopCapturer()).toBe(true); expect(platform.supportsDesktopCapturer()).toBe(true);
}); });
it('indicates no support for jitsi screensharing', () => { it("indicates no support for jitsi screensharing", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.supportsJitsiScreensharing()).toBe(false); expect(platform.supportsJitsiScreensharing()).toBe(false);
}); });
describe('notifications', () => { describe("notifications", () => {
it('indicates support for notifications', () => { it("indicates support for notifications", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.supportsNotifications()).toBe(true); expect(platform.supportsNotifications()).toBe(true);
}); });
it('may send notifications', () => { it("may send notifications", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.maySendNotifications()).toBe(true); expect(platform.maySendNotifications()).toBe(true);
}); });
it('pretends to request notification permission', async () => { it("pretends to request notification permission", async () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
const result = await platform.requestNotificationPermission(); const result = await platform.requestNotificationPermission();
expect(result).toEqual('granted'); expect(result).toEqual("granted");
}); });
it('creates a loud notification', async () => { it("creates a loud notification", async () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
platform.loudNotification(new MatrixEvent(), new Room('!room:server', {} as any, userId)); platform.loudNotification(new MatrixEvent(), new Room("!room:server", {} as any, userId));
expect(mockElectron.send).toHaveBeenCalledWith('loudNotification'); expect(mockElectron.send).toHaveBeenCalledWith("loudNotification");
}); });
it('sets notification count when count is changing', async () => { it("sets notification count when count is changing", async () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
platform.setNotificationCount(0); platform.setNotificationCount(0);
// not called because matches internal notificaiton count // not called because matches internal notificaiton count
expect(mockElectron.send).not.toHaveBeenCalledWith('setBadgeCount', 0); expect(mockElectron.send).not.toHaveBeenCalledWith("setBadgeCount", 0);
platform.setNotificationCount(1); platform.setNotificationCount(1);
expect(mockElectron.send).toHaveBeenCalledWith('setBadgeCount', 1); expect(mockElectron.send).toHaveBeenCalledWith("setBadgeCount", 1);
}); });
}); });
describe('spellcheck', () => { describe("spellcheck", () => {
it('indicates support for spellcheck settings', () => { it("indicates support for spellcheck settings", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
expect(platform.supportsSpellCheckSettings()).toBe(true); expect(platform.supportsSpellCheckSettings()).toBe(true);
}); });
it('gets available spellcheck languages', () => { it("gets available spellcheck languages", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
mockElectron.send.mockClear(); mockElectron.send.mockClear();
platform.getAvailableSpellCheckLanguages(); platform.getAvailableSpellCheckLanguages();
const [channel, { name }] = mockElectron.send.mock.calls[0]; const [channel, { name }] = mockElectron.send.mock.calls[0];
expect(channel).toEqual("ipcCall"); expect(channel).toEqual("ipcCall");
expect(name).toEqual('getAvailableSpellCheckLanguages'); expect(name).toEqual("getAvailableSpellCheckLanguages");
}); });
}); });
describe('pickle key', () => { describe("pickle key", () => {
it('makes correct ipc call to get pickle key', () => { it("makes correct ipc call to get pickle key", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
mockElectron.send.mockClear(); mockElectron.send.mockClear();
platform.getPickleKey(userId, deviceId); platform.getPickleKey(userId, deviceId);
const [, { name, args }] = mockElectron.send.mock.calls[0]; const [, { name, args }] = mockElectron.send.mock.calls[0];
expect(name).toEqual('getPickleKey'); expect(name).toEqual("getPickleKey");
expect(args).toEqual([userId, deviceId]); expect(args).toEqual([userId, deviceId]);
}); });
it('makes correct ipc call to create pickle key', () => { it("makes correct ipc call to create pickle key", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
mockElectron.send.mockClear(); mockElectron.send.mockClear();
platform.createPickleKey(userId, deviceId); platform.createPickleKey(userId, deviceId);
const [, { name, args }] = mockElectron.send.mock.calls[0]; const [, { name, args }] = mockElectron.send.mock.calls[0];
expect(name).toEqual('createPickleKey'); expect(name).toEqual("createPickleKey");
expect(args).toEqual([userId, deviceId]); expect(args).toEqual([userId, deviceId]);
}); });
it('makes correct ipc call to destroy pickle key', () => { it("makes correct ipc call to destroy pickle key", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
mockElectron.send.mockClear(); mockElectron.send.mockClear();
platform.destroyPickleKey(userId, deviceId); platform.destroyPickleKey(userId, deviceId);
const [, { name, args }] = mockElectron.send.mock.calls[0]; const [, { name, args }] = mockElectron.send.mock.calls[0];
expect(name).toEqual('destroyPickleKey'); expect(name).toEqual("destroyPickleKey");
expect(args).toEqual([userId, deviceId]); expect(args).toEqual([userId, deviceId]);
}); });
}); });
describe('versions', () => { describe("versions", () => {
it('calls install update', () => { it("calls install update", () => {
const platform = new ElectronPlatform(); const platform = new ElectronPlatform();
platform.installUpdate(); platform.installUpdate();
expect(mockElectron.send).toHaveBeenCalledWith('install_update'); expect(mockElectron.send).toHaveBeenCalledWith("install_update");
}); });
}); });
}); });

View File

@ -21,7 +21,7 @@ import WebPlatform from "../../../../src/vector/platform/WebPlatform";
jest.mock("../../../../src/vector/platform/WebPlatform"); jest.mock("../../../../src/vector/platform/WebPlatform");
describe('PWAPlatform', () => { describe("PWAPlatform", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
@ -54,7 +54,7 @@ describe('PWAPlatform', () => {
}); });
it("should handle Navigator::setAppBadge rejecting gracefully", () => { it("should handle Navigator::setAppBadge rejecting gracefully", () => {
navigator.setAppBadge = jest.fn().mockRejectedValue(new Error); navigator.setAppBadge = jest.fn().mockRejectedValue(new Error());
const platform = new PWAPlatform(); const platform = new PWAPlatform();
expect(() => platform.setNotificationCount(123)).not.toThrow(); expect(() => platform.setNotificationCount(123)).not.toThrow();
}); });

View File

@ -15,24 +15,24 @@ limitations under the License.
*/ */
import fetchMock from "fetch-mock-jest"; import fetchMock from "fetch-mock-jest";
import { UpdateCheckStatus } from 'matrix-react-sdk/src/BasePlatform'; import { UpdateCheckStatus } from "matrix-react-sdk/src/BasePlatform";
import { MatrixClientPeg } from 'matrix-react-sdk/src/MatrixClientPeg'; import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg";
import WebPlatform from '../../../../src/vector/platform/WebPlatform'; import WebPlatform from "../../../../src/vector/platform/WebPlatform";
fetchMock.config.overwriteRoutes = true; fetchMock.config.overwriteRoutes = true;
describe('WebPlatform', () => { describe("WebPlatform", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it('returns human readable name', () => { it("returns human readable name", () => {
const platform = new WebPlatform(); const platform = new WebPlatform();
expect(platform.getHumanReadableName()).toEqual('Web Platform'); expect(platform.getHumanReadableName()).toEqual("Web Platform");
}); });
it('registers service worker', () => { it("registers service worker", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - mocking readonly object // @ts-ignore - mocking readonly object
navigator.serviceWorker = { register: jest.fn() }; navigator.serviceWorker = { register: jest.fn() };
@ -65,12 +65,14 @@ describe('WebPlatform', () => {
}); });
describe("getDefaultDeviceDisplayName", () => { describe("getDefaultDeviceDisplayName", () => {
it.each([[ it.each([
"https://develop.element.io/#/room/!foo:bar", [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) " + "https://develop.element.io/#/room/!foo:bar",
"Chrome/105.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) " +
"develop.element.io: Chrome on macOS", "Chrome/105.0.0.0 Safari/537.36",
]])("%s & %s = %s", (url, userAgent, result) => { "develop.element.io: Chrome on macOS",
],
])("%s & %s = %s", (url, userAgent, result) => {
delete window.navigator; delete window.navigator;
window.navigator = { userAgent } as unknown as Navigator; window.navigator = { userAgent } as unknown as Navigator;
delete window.location; delete window.location;
@ -80,66 +82,66 @@ describe('WebPlatform', () => {
}); });
}); });
describe('notification support', () => { describe("notification support", () => {
const mockNotification = { const mockNotification = {
requestPermission: jest.fn(), requestPermission: jest.fn(),
permission: 'notGranted', permission: "notGranted",
}; };
beforeEach(() => { beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.Notification = mockNotification; window.Notification = mockNotification;
mockNotification.permission = 'notGranted'; mockNotification.permission = "notGranted";
}); });
it('supportsNotifications returns false when platform does not support notifications', () => { it("supportsNotifications returns false when platform does not support notifications", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.Notification = undefined; window.Notification = undefined;
expect(new WebPlatform().supportsNotifications()).toBe(false); expect(new WebPlatform().supportsNotifications()).toBe(false);
}); });
it('supportsNotifications returns true when platform supports notifications', () => { it("supportsNotifications returns true when platform supports notifications", () => {
expect(new WebPlatform().supportsNotifications()).toBe(true); expect(new WebPlatform().supportsNotifications()).toBe(true);
}); });
it('maySendNotifications returns true when notification permissions are not granted', () => { it("maySendNotifications returns true when notification permissions are not granted", () => {
expect(new WebPlatform().maySendNotifications()).toBe(false); expect(new WebPlatform().maySendNotifications()).toBe(false);
}); });
it('maySendNotifications returns true when notification permissions are granted', () => { it("maySendNotifications returns true when notification permissions are granted", () => {
mockNotification.permission = 'granted'; mockNotification.permission = "granted";
expect(new WebPlatform().maySendNotifications()).toBe(true); expect(new WebPlatform().maySendNotifications()).toBe(true);
}); });
it('requests notification permissions and returns result ', async () => { it("requests notification permissions and returns result ", async () => {
mockNotification.requestPermission.mockImplementation(callback => callback('test')); mockNotification.requestPermission.mockImplementation((callback) => callback("test"));
const platform = new WebPlatform(); const platform = new WebPlatform();
const result = await platform.requestNotificationPermission(); const result = await platform.requestNotificationPermission();
expect(result).toEqual('test'); expect(result).toEqual("test");
}); });
}); });
describe('app version', () => { describe("app version", () => {
const envVersion = process.env.VERSION; const envVersion = process.env.VERSION;
const prodVersion = '1.10.13'; const prodVersion = "1.10.13";
beforeEach(() => { beforeEach(() => {
jest.spyOn(MatrixClientPeg, 'userRegisteredWithinLastHours').mockReturnValue(false); jest.spyOn(MatrixClientPeg, "userRegisteredWithinLastHours").mockReturnValue(false);
}); });
afterAll(() => { afterAll(() => {
process.env.VERSION = envVersion; process.env.VERSION = envVersion;
}); });
it('should return true from canSelfUpdate()', async () => { it("should return true from canSelfUpdate()", async () => {
const platform = new WebPlatform(); const platform = new WebPlatform();
const result = await platform.canSelfUpdate(); const result = await platform.canSelfUpdate();
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('getAppVersion returns normalized app version', async () => { it("getAppVersion returns normalized app version", async () => {
process.env.VERSION = prodVersion; process.env.VERSION = prodVersion;
const platform = new WebPlatform(); const platform = new WebPlatform();
@ -156,23 +158,26 @@ describe('WebPlatform', () => {
expect(notSemverVersion).toEqual(`version not like semver`); expect(notSemverVersion).toEqual(`version not like semver`);
}); });
describe('pollForUpdate()', () => { describe("pollForUpdate()", () => {
it('should return not available and call showNoUpdate when current version ' + it(
'matches most recent version', async () => { "should return not available and call showNoUpdate when current version " +
process.env.VERSION = prodVersion; "matches most recent version",
fetchMock.getOnce("/version", prodVersion); async () => {
const platform = new WebPlatform(); process.env.VERSION = prodVersion;
fetchMock.getOnce("/version", prodVersion);
const platform = new WebPlatform();
const showUpdate = jest.fn(); const showUpdate = jest.fn();
const showNoUpdate = jest.fn(); const showNoUpdate = jest.fn();
const result = await platform.pollForUpdate(showUpdate, showNoUpdate); const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable }); expect(result).toEqual({ status: UpdateCheckStatus.NotAvailable });
expect(showUpdate).not.toHaveBeenCalled(); expect(showUpdate).not.toHaveBeenCalled();
expect(showNoUpdate).toHaveBeenCalled(); expect(showNoUpdate).toHaveBeenCalled();
}); },
);
it('should strip v prefix from versions before comparing', async () => { it("should strip v prefix from versions before comparing", async () => {
process.env.VERSION = prodVersion; process.env.VERSION = prodVersion;
fetchMock.getOnce("/version", `v${prodVersion}`); fetchMock.getOnce("/version", `v${prodVersion}`);
const platform = new WebPlatform(); const platform = new WebPlatform();
@ -187,24 +192,26 @@ describe('WebPlatform', () => {
expect(showNoUpdate).toHaveBeenCalled(); expect(showNoUpdate).toHaveBeenCalled();
}); });
it('should return ready and call showUpdate when current version ' + it(
'differs from most recent version', async () => { "should return ready and call showUpdate when current version " + "differs from most recent version",
process.env.VERSION = '0.0.0'; // old version async () => {
fetchMock.getOnce("/version", prodVersion); process.env.VERSION = "0.0.0"; // old version
const platform = new WebPlatform(); fetchMock.getOnce("/version", prodVersion);
const platform = new WebPlatform();
const showUpdate = jest.fn(); const showUpdate = jest.fn();
const showNoUpdate = jest.fn(); const showNoUpdate = jest.fn();
const result = await platform.pollForUpdate(showUpdate, showNoUpdate); const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
expect(result).toEqual({ status: UpdateCheckStatus.Ready }); expect(result).toEqual({ status: UpdateCheckStatus.Ready });
expect(showUpdate).toHaveBeenCalledWith('0.0.0', prodVersion); expect(showUpdate).toHaveBeenCalledWith("0.0.0", prodVersion);
expect(showNoUpdate).not.toHaveBeenCalled(); expect(showNoUpdate).not.toHaveBeenCalled();
}); },
);
it('should return ready without showing update when user registered in last 24', async () => { it("should return ready without showing update when user registered in last 24", async () => {
process.env.VERSION = '0.0.0'; // old version process.env.VERSION = "0.0.0"; // old version
jest.spyOn(MatrixClientPeg, 'userRegisteredWithinLastHours').mockReturnValue(true); jest.spyOn(MatrixClientPeg, "userRegisteredWithinLastHours").mockReturnValue(true);
fetchMock.getOnce("/version", prodVersion); fetchMock.getOnce("/version", prodVersion);
const platform = new WebPlatform(); const platform = new WebPlatform();
@ -217,7 +224,7 @@ describe('WebPlatform', () => {
expect(showNoUpdate).not.toHaveBeenCalled(); expect(showNoUpdate).not.toHaveBeenCalled();
}); });
it('should return error when version check fails', async () => { it("should return error when version check fails", async () => {
fetchMock.getOnce("/version", { throws: "oups" }); fetchMock.getOnce("/version", { throws: "oups" });
const platform = new WebPlatform(); const platform = new WebPlatform();
@ -225,7 +232,7 @@ describe('WebPlatform', () => {
const showNoUpdate = jest.fn(); const showNoUpdate = jest.fn();
const result = await platform.pollForUpdate(showUpdate, showNoUpdate); const result = await platform.pollForUpdate(showUpdate, showNoUpdate);
expect(result).toEqual({ status: UpdateCheckStatus.Error, detail: 'Unknown Error' }); expect(result).toEqual({ status: UpdateCheckStatus.Error, detail: "Unknown Error" });
expect(showUpdate).not.toHaveBeenCalled(); expect(showUpdate).not.toHaveBeenCalled();
expect(showNoUpdate).not.toHaveBeenCalled(); expect(showNoUpdate).not.toHaveBeenCalled();
}); });

Some files were not shown because too many files have changed in this diff Show More