Chat Tests Static Analysis Localazy Quality Gate Status Coverage Vulnerabilities Bugs

Element

Element (formerly known as Vector and Riot) is a Matrix web client built using the Matrix JS SDK.

Supported Environments

Element has several tiers of support for different environments:

  • Supported
    • Definition:
      • Issues actively triaged, regressions block the release
    • Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes
    • Last 2 versions of Safari
    • Latest release of official Element Desktop app on desktop OSes
    • Desktop OSes means macOS, Windows, and Linux versions for desktop devices that are actively supported by the OS vendor and receive security updates
  • Best effort
    • Definition:
      • Issues accepted, regressions do not block the release
      • The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
      • The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
    • Last major release of Firefox ESR and Chrome/Edge Extended Stable
  • Community Supported
    • Definition:
      • Issues accepted, regressions do not block the release
      • Community contributions are welcome to support these issues
    • Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS
  • Not supported
    • Definition: Issues only affecting unsupported environments are closed
    • Everything else

The period of support for these tiers should last until the releases specified above, plus 1 app release cycle(2 weeks). In the case of Firefox ESR this is extended further to allow it land in Debian Stable.

For accessing Element on an Android or iOS device, we currently recommend the native apps element-android and element-ios.

Getting Started

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 for those who like living dangerously.

To host your own instance of Element see Installing Element Web.

To install Element as a desktop application, see Running as a desktop app below.

Important Security Notes

Separate domains

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) vulnerabilities that could occur if someone caused Element to load and render malicious user generated content from a Matrix API which then had trusted 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 situation, but it's still not good practice to do it in the first place. See https://github.com/element-hq/element-web/issues/1977 for more details.

Configuration best practices

Unless you have special requirements, you will want to add the following to your web server configuration when hosting Element Web:

  • The X-Frame-Options: SAMEORIGIN header, to prevent Element Web from being framed and protect from clickjacking.
  • The frame-ancestors 'self' directive to your Content-Security-Policy header, as the modern replacement for X-Frame-Options (though both should be included since not all browsers support it yet, see this).
  • The X-Content-Type-Options: nosniff header, to disable MIME sniffing.
  • The X-XSS-Protection: 1; mode=block; header, for basic XSS protection in legacy browsers.

If you are using nginx, this would look something like the following:

add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "frame-ancestors 'self'";

For Apache, the configuration looks like:

Header set X-Frame-Options SAMEORIGIN
Header set X-Content-Type-Options nosniff
Header set X-XSS-Protection "1; mode=block"
Header set Content-Security-Policy "frame-ancestors 'self'"

Note: In case you are already setting a Content-Security-Policy header elsewhere, you should modify it to include the frame-ancestors directive instead of adding that last line.

Building From Source

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.

Using yarn instead of npm is recommended. Please see the Yarn install guide if you do not have it already.

  1. Install or update node.js so that your node is at least the current recommended LTS.
  2. Install yarn if not present already.
  3. Clone the repo: git clone https://github.com/element-hq/element-web.git.
  4. Switch to the element-web directory: cd element-web.
  5. Install the prerequisites: yarn install.
  6. Configure the app by copying config.sample.json to config.json and modifying it. See the configuration docs for details.
  7. yarn dist to build a tarball to deploy. Untaring this file will give a version-specific directory containing all the files that need to go on your web server.

Note that yarn dist is not supported on Windows, so Windows users can run yarn build, which will build all the necessary files into the webapp directory. The version of Element will not appear in Settings without using the dist script. You can then mount the webapp directory on your web server to actually serve up the app, which is entirely static content.

Running as a Desktop app

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, build it yourself.

To build it yourself, follow the instructions at https://github.com/element-hq/element-desktop.

Many thanks to @aviraldg for the initial work on the Electron integration.

The configuration docs show how to override the desktop app's default settings if desired.

config.json

Element supports a variety of settings to configure default servers, behaviour, themes, etc. See the configuration docs for more details.

Labs Features

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.

Caching requirements

Element requires the following URLs not to be cached, when/if you are serving Element from your own webserver:

/config.*.json
/i18n
/home
/sites
/index.html

We also recommend that you force browsers to re-validate any cached copy of Element on page load by configuring your webserver to return Cache-Control: no-cache for /. This ensures the browser will fetch a new version of Element on the next page load after it's been deployed. Note that this is already configured for you in the nginx config of our Dockerfile.

Development

Before attempting to develop on Element you must read the developer guide for matrix-react-sdk, which also defines the design, architecture and style for Element too.

Read the Choosing an issue page for some guidance about where to start. Before starting work on a feature, it's best to ensure your plan aligns well with our vision for Element. Please chat with the team in #element-dev:matrix.org before you start so we can ensure it's something we'd be willing to merge.

You should also familiarise yourself with the "Here be Dragons" guide to the tame & not-so-tame dragons (gotchas) which exist in the codebase.

The idea of Element is to be a relatively lightweight "skin" of customisations on top of the underlying matrix-react-sdk. matrix-react-sdk provides both the higher and lower level React components useful for building Matrix communication apps using React.

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) hosted by external CDNs or servers but instead please package all dependencies into Element itself.

Setting up a dev environment

Much of the functionality in Element is actually in the matrix-js-sdk module. It is possible to set these up in a way that makes it easy to track the develop branches in git and to make local changes without having to manually rebuild each time.

First clone and build matrix-js-sdk:

git clone https://github.com/matrix-org/matrix-js-sdk.git
pushd matrix-js-sdk
yarn link
yarn install
popd

Clone the repo and switch to the element-web directory:

git clone https://github.com/element-hq/element-web.git
cd element-web

Configure the app by copying config.sample.json to config.json and modifying it. See the configuration docs for details.

Finally, build and start Element itself:

yarn link matrix-js-sdk
yarn install
yarn start

Wait a few seconds for the initial build to finish; you should see something like:

[element-js] <s> [webpack.Progress] 100%
[element-js]
[element-js] ℹ 「wdm」:    1840 modules
[element-js] ℹ 「wdm」: Compiled successfully.

Remember, the command will not terminate since it runs the web server and rebuilds source files when they change. This development server also 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.

Note: The build script uses inotify by default on Linux to monitor directories for changes. If the inotify limits are too low your build will fail silently or with Error: EMFILE: too many open files. To avoid these issues, we recommend a watch limit of at least 128M and instance limit around 512.

You may be interested in issues #15750 and #15774 for further details.

To set a new inotify watch and instance limit, execute:

sudo sysctl fs.inotify.max_user_watches=131072
sudo sysctl fs.inotify.max_user_instances=512
sudo sysctl -p

If you wish, you can make the new limits permanent, by executing:

echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

When you make changes to matrix-js-sdk they should be automatically picked up by webpack and built.

If any of these steps error with, file table overflow, you are probably on a mac 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.

Running the tests

There are a number of application-level tests in the tests directory; these are designed to run with Jest and JSDOM. To run them

yarn test

End-to-End tests

See matrix-react-sdk for how to run the end-to-end tests.

Translations

To add a new translation, head to the translating doc.

For a developer guide, see the translating dev doc.

Triaging issues

Issues are triaged by community members and the Web App Team, following the triage process.

We use issue labels to sort all incoming issues.

Chat Tests Static Analysis Localazy Quality Gate Status Coverage Vulnerabilities Bugs

Element

Element (formerly known as Vector and Riot) is a Matrix web client built using the Matrix JS SDK.

Supported Environments

Element has several tiers of support for different environments:

  • Supported
    • Definition:
      • Issues actively triaged, regressions block the release
    • Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes
    • Last 2 versions of Safari
    • Latest release of official Element Desktop app on desktop OSes
    • Desktop OSes means macOS, Windows, and Linux versions for desktop devices that are actively supported by the OS vendor and receive security updates
  • Best effort
    • Definition:
      • Issues accepted, regressions do not block the release
      • The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
      • The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
    • Last major release of Firefox ESR and Chrome/Edge Extended Stable
  • Community Supported
    • Definition:
      • Issues accepted, regressions do not block the release
      • Community contributions are welcome to support these issues
    • Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS
  • Not supported
    • Definition: Issues only affecting unsupported environments are closed
    • Everything else

The period of support for these tiers should last until the releases specified above, plus 1 app release cycle(2 weeks). In the case of Firefox ESR this is extended further to allow it land in Debian Stable.

For accessing Element on an Android or iOS device, we currently recommend the native apps element-android and element-ios.

Getting Started

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 for those who like living dangerously.

To host your own instance of Element see Installing Element Web.

To install Element as a desktop application, see Running as a desktop app below.

Important Security Notes

Separate domains

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) vulnerabilities that could occur if someone caused Element to load and render malicious user generated content from a Matrix API which then had trusted 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 situation, but it's still not good practice to do it in the first place. See https://github.com/element-hq/element-web/issues/1977 for more details.

Configuration best practices

Unless you have special requirements, you will want to add the following to your web server configuration when hosting Element Web:

  • The X-Frame-Options: SAMEORIGIN header, to prevent Element Web from being framed and protect from clickjacking.
  • The frame-ancestors 'self' directive to your Content-Security-Policy header, as the modern replacement for X-Frame-Options (though both should be included since not all browsers support it yet, see this).
  • The X-Content-Type-Options: nosniff header, to disable MIME sniffing.
  • The X-XSS-Protection: 1; mode=block; header, for basic XSS protection in legacy browsers.

If you are using nginx, this would look something like the following:

add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "frame-ancestors 'self'";

For Apache, the configuration looks like:

Header set X-Frame-Options SAMEORIGIN
Header set X-Content-Type-Options nosniff
Header set X-XSS-Protection "1; mode=block"
Header set Content-Security-Policy "frame-ancestors 'self'"

Note: In case you are already setting a Content-Security-Policy header elsewhere, you should modify it to include the frame-ancestors directive instead of adding that last line.

Building From Source

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.

Using yarn instead of npm is recommended. Please see the Yarn install guide if you do not have it already.

  1. Install or update node.js so that your node is at least the current recommended LTS.
  2. Install yarn if not present already.
  3. Clone the repo: git clone https://github.com/element-hq/element-web.git.
  4. Switch to the element-web directory: cd element-web.
  5. Install the prerequisites: yarn install.
  6. Configure the app by copying config.sample.json to config.json and modifying it. See the configuration docs for details.
  7. yarn dist to build a tarball to deploy. Untaring this file will give a version-specific directory containing all the files that need to go on your web server.

Note that yarn dist is not supported on Windows, so Windows users can run yarn build, which will build all the necessary files into the webapp directory. The version of Element will not appear in Settings without using the dist script. You can then mount the webapp directory on your web server to actually serve up the app, which is entirely static content.

Running as a Desktop app

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, build it yourself.

To build it yourself, follow the instructions at https://github.com/element-hq/element-desktop.

Many thanks to @aviraldg for the initial work on the Electron integration.

The configuration docs show how to override the desktop app's default settings if desired.

config.json

Element supports a variety of settings to configure default servers, behaviour, themes, etc. See the configuration docs for more details.

Labs Features

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.

Caching requirements

Element requires the following URLs not to be cached, when/if you are serving Element from your own webserver:

/config.*.json
/i18n
/home
/sites
/index.html

We also recommend that you force browsers to re-validate any cached copy of Element on page load by configuring your webserver to return Cache-Control: no-cache for /. This ensures the browser will fetch a new version of Element on the next page load after it's been deployed. Note that this is already configured for you in the nginx config of our Dockerfile.

Development

Before attempting to develop on Element you must read the developer guide for matrix-react-sdk, which also defines the design, architecture and style for Element too.

Read the Choosing an issue page for some guidance about where to start. Before starting work on a feature, it's best to ensure your plan aligns well with our vision for Element. Please chat with the team in #element-dev:matrix.org before you start so we can ensure it's something we'd be willing to merge.

You should also familiarise yourself with the "Here be Dragons" guide to the tame & not-so-tame dragons (gotchas) which exist in the codebase.

The idea of Element is to be a relatively lightweight "skin" of customisations on top of the underlying matrix-react-sdk. matrix-react-sdk provides both the higher and lower level React components useful for building Matrix communication apps using React.

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) hosted by external CDNs or servers but instead please package all dependencies into Element itself.

Setting up a dev environment

Much of the functionality in Element is actually in the matrix-js-sdk module. It is possible to set these up in a way that makes it easy to track the develop branches in git and to make local changes without having to manually rebuild each time.

First clone and build matrix-js-sdk:

git clone https://github.com/matrix-org/matrix-js-sdk.git
pushd matrix-js-sdk
yarn link
yarn install
popd

Clone the repo and switch to the element-web directory:

git clone https://github.com/element-hq/element-web.git
cd element-web

Configure the app by copying config.sample.json to config.json and modifying it. See the configuration docs for details.

Finally, build and start Element itself:

yarn link matrix-js-sdk
yarn install
yarn start

Wait a few seconds for the initial build to finish; you should see something like:

[element-js] <s> [webpack.Progress] 100%
[element-js]
[element-js] ℹ 「wdm」:    1840 modules
[element-js] ℹ 「wdm」: Compiled successfully.

Remember, the command will not terminate since it runs the web server and rebuilds source files when they change. This development server also 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.

Note: The build script uses inotify by default on Linux to monitor directories for changes. If the inotify limits are too low your build will fail silently or with Error: EMFILE: too many open files. To avoid these issues, we recommend a watch limit of at least 128M and instance limit around 512.

You may be interested in issues #15750 and #15774 for further details.

To set a new inotify watch and instance limit, execute:

sudo sysctl fs.inotify.max_user_watches=131072
sudo sysctl fs.inotify.max_user_instances=512
sudo sysctl -p

If you wish, you can make the new limits permanent, by executing:

echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

When you make changes to matrix-js-sdk they should be automatically picked up by webpack and built.

If any of these steps error with, file table overflow, you are probably on a mac 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.

Running the tests

There are a number of application-level tests in the tests directory; these are designed to run with Jest and JSDOM. To run them

yarn test

End-to-End tests

See matrix-react-sdk for how to run the end-to-end tests.

Translations

To add a new translation, head to the translating doc.

For a developer guide, see the translating dev doc.

Triaging issues

Issues are triaged by community members and the Web App Team, following the triage process.

We use issue labels to sort all incoming issues.

Beta features

Beta features are features that are not ready for production yet but the team wants more people to try the features and give feedback on them.

Before a feature gets into its beta phase, it is often a labs feature (see Labs).

Be warned! Beta features may not be completely finalised or stable!

Video rooms (feature_video_rooms)

Enables support for creating and joining video rooms, which are persistent video chats that users can jump in and out of.

Labs features

If Labs is enabled in the Element config, you can enable some of these features by going to Settings->Labs. This list is non-exhaustive and subject to change, chat in #element-web:matrix.org for more information.

If a labs features gets more stable, it may be promoted to a beta feature (see Betas).

Be warned! Labs features are not finalised, they may be fragile, they may change, they may be dropped. Ask in the room if you are unclear about any details here.

Submit Abuse Report to Moderators MSC3215 support (feature_report_to_moderators)

A new version of the "Report" dialog that lets users send abuse reports directly to room moderators, if the room supports it.

Render LaTeX maths in messages (feature_latex_maths)

Enables rendering of LaTeX maths in messages using KaTeX. LaTeX between single dollar-signs is interpreted as inline maths and double dollar-signs as display maths (i.e. centred on its own line).

Message pinning (feature_pinning)

Allows you to pin messages in the room. To pin a message, use the 3 dots to the right of the message and select "Pin".

Jump to date (feature_jump_to_date)

Note: This labs feature is only visible when your homeserver has MSC3030 enabled (in Synapse, add experimental_features -> msc3030_enabled to your homeserver.yaml) which means GET /_matrix/client/versions responds with org.matrix.msc3030 under the unstable_features key.

Adds a dropdown menu to the date separator headers in the timeline which allows you to jump to last week, last month, the beginning of the room, or choose a date from the calendar.

Also adds the /jumptodate 2022-01-31 slash command.

New ways to ignore people (feature_mjolnir)

When enabled, a new settings tab appears for users to be able to manage their ban lists. This is a different kind of ignoring where the ignored user's messages still get rendered, but are hidden by default.

Ban lists are rooms within Matrix, proposed as MSC2313. Mjolnir is a set of moderation tools which support ban lists.

Verifications in DMs (feature_dm_verification)

An implementation of MSC2241. When enabled, verification might not work with devices which don't support MSC2241.

This also includes a new implementation of the user & member info panel, designed to share more code between showing community members & room members. Built on top of this new panel is also a new UX for verification from the member panel.

The setting will be removed in a future release, enabling it non-optionally for all users.

Bridge info tab (feature_bridge_state)

Adds a "Bridge Info" tab to the Room Settings dialog, if a compatible bridge is present in the room. The Bridge info tab pulls information from the m.bridge state event (MSC2346). Since the feature is based upon a MSC, most bridges are not expected to be compatible, and users should not rely on this tab as the single source of truth just yet.

Custom themes (feature_custom_themes)

Custom themes are possible through Element's theme support, though normally these themes need to be defined in the config for Element. This labs flag adds an ability for end users to add themes themselves by using a URL to the JSON theme definition.

For some sample themes, check out aaronraimist/element-themes.

Live location sharing (feature_location_share_live) [In Development]

Enables sharing your current location to the timeline, with live updates.

Video rooms (feature_video_rooms)

Enables support for creating video rooms, which are persistent video chats that users can jump in and out of.

Element Call video rooms (feature_element_call_video_rooms) [In Development]

Enables support for video rooms that use Element Call rather than Jitsi, and causes the 'New video room' option to create Element Call video rooms rather than Jitsi ones.

This flag will not have any effect unless feature_video_rooms is also enabled.

New group call experience (feature_group_calls) [In Development]

This feature allows users to place native MSC3401 group calls in compatible rooms, using Element Call.

If you're enabling this at the deployment level, you may also want to reference the docs for the element_call config section.

Disable per-sender encryption for Element Call (feature_disable_call_per_sender_encryption)

The default for embedded Element Call in Element Web is per-participant encryption. This labs flag disables encryption for embedded Element Call in encrypted rooms.

Under the hood this stops Element Web from adding the perParticipantE2EE flag for the Element Call widget url.

This is useful while we experiment with encryption and to make calling compatible with platforms that don't use encryption yet.

Rich text in room topics (feature_html_topic) [In Development]

Enables rendering of MD / HTML in room topics.

Enable the notifications panel in the room header (feature_notifications)

Unreliable in encrypted rooms.

Knock rooms (feature_ask_to_join) [In Development]

Enables knock feature for rooms. This allows users to ask to join a room.

Installing Element Web

Familiarise yourself with the Important Security Notes before starting, they apply to all installation methods.

Note: that for the security of your chats will need to serve Element over HTTPS. Major browsers also do not allow you to use VoIP/video chats over HTTP, as WebRTC is only usable over HTTPS. There are some exceptions like when using localhost, which is considered a secure context and thus allowed.

Release tarball

  1. Download the latest version from https://github.com/element-hq/element-web/releases
  2. Untar the tarball on your web server
  3. Move (or symlink) the element-x.x.x directory to an appropriate name
  4. Configure the correct caching headers in your webserver (see below)
  5. Configure the app by copying config.sample.json to config.json and modifying it. See the configuration docs for details.
  6. Enter the URL into your browser and log into Element!

Releases are signed using gpg and the OpenPGP standard, and can be checked against the public key located at https://packages.element.io/element-release-key.asc.

Debian package

Element Web is now also available as a Debian package for Debian and Ubuntu based systems.

sudo apt install -y wget apt-transport-https
sudo wget -O /usr/share/keyrings/element-io-archive-keyring.gpg https://packages.element.io/debian/element-io-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/element-io-archive-keyring.gpg] https://packages.element.io/debian/ default main" | sudo tee /etc/apt/sources.list.d/element-io.list
sudo apt update
sudo apt install element-web

Configure the app by modifying /etc/element-web/config.json. See the configuration docs for details.

Then point your chosen web server (e.g. Caddy, Nginx, Apache, etc) at the /usr/share/element-web webroot.

Docker

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:

docker run --rm -p 127.0.0.1:80:80 vectorim/element-web

A server can also be made available to clients outside the local host by omitting the explicit local address as described in docker run documentation:

docker run --rm -p 80:80 vectorim/element-web

To supply your own custom config.json, map a volume to /app/config.json. For example, if your custom config was located at /etc/element-web/config.json then your Docker command would be:

docker run --rm -p 127.0.0.1:80:80 -v /etc/element-web/config.json:/app/config.json vectorim/element-web

To build the image yourself:

git clone https://github.com/element-hq/element-web.git element-web
cd element-web
git checkout master
docker build .

If you're building a custom branch, or want to use the develop branch, check out the appropriate element-web branch and then run:

docker build -t \
    --build-arg USE_CUSTOM_SDKS=true \
    --build-arg JS_SDK_REPO="https://github.com/matrix-org/matrix-js-sdk.git" \
    --build-arg JS_SDK_BRANCH="develop" \
    .

Kubernetes

The provided element-web docker image can also be run from within a Kubernetes cluster. See the Kubernetes example for more details.

Configuration

🦖 Deprecation notice

Configuration keys were previously a mix of camelCase and snake_case. We standardised to snake_case but added compatibility for camelCase to all settings. This backwards compatibility will be getting removed in a future release so please ensure you are using snake_case.


You can configure the app by copying config.sample.json to config.json or config.$domain.json and customising it. Element will attempt to load first config.$domain.json and if it fails config.json. This mechanism allows different configuration options depending on if you're hitting e.g. app1.example.com or app2.example.com. Configs are not mixed in any way, it either entirely uses the domain config, or entirely uses config.json.

The possible configuration options are described here. If you run into issues, please visit #element-web:matrix.org on Matrix.

For a good example of a production-tuned config, see https://app.element.io/config.json

For an example of a development/beta-tuned config, see https://develop.element.io/config.json

After changing the config, the app will need to be reloaded. For web browsers this is a simple page refresh, however for the desktop app the application will need to be exited fully (including via the task tray) and re-started.

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 able to use a different homeserver if they like, though this can be disabled with "disable_custom_urls": true in your config.

One of the following options must be supplied:

  1. default_server_config: The preferred method of setting the homeserver connection information. Simply copy/paste your /.well-known/matrix/client into this field. For example:
    {
        "default_server_config": {
            "m.homeserver": {
                "base_url": "https://matrix-client.matrix.org"
            },
            "m.identity_server": {
                "base_url": "https://vector.im"
            }
        }
    }
    
  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): "default_server_name": "matrix.org"
  3. default_hs_url and (optionally) default_is_url: A very deprecated method of defining the connection information. These are the same values seen as base_url in the default_server_config example, with default_is_url being optional.

If both default_server_config and default_server_name are used, Element will try to look up the connection information using .well-known, and if that fails, take default_server_config as the homeserver connection information.

Labs flags

Labs flags are optional, typically beta or in-development, features that can be turned on or off. The full range of labs flags and their development status are documented here. If interested, the feature flag process is documented here.

To force a labs flag on or off, use the following:

{
    "features": {
        "feature_you_want_to_turn_on": true,
        "feature_you_want_to_keep_off": false
    }
}

If you'd like the user to be able to self-select which labs flags they can turn on, add "show_labs_settings": true to your config. This will turn on the tab in user settings.

Note: Feature support varies release-by-release. Check the labs flag documentation frequently if enabling the functionality.

Default settings

Some settings additionally support being specified at the config level to affect the user experience of your Element Web instance. As of writing those settings are not fully documented, however a few are:

  1. default_federate: When true (default), rooms will be marked as "federatable" during creation. Typically this setting shouldn't be used as the federation capabilities of a room cannot be changed after the room is created.
  2. default_country_code: An optional ISO 3166 alpha2 country code (eg: GB, the default) to use when showing phone number inputs.
  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:
    {
        "room_directory": {
            "servers": ["matrix.org", "example.org"]
        }
    }
    
  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:
    {
        "setting_defaults": {
            "MessageComposerInput.showStickersButton": false,
            "MessageComposerInput.showPollsButton": false
        }
    }
    
    These values will take priority over the hardcoded defaults for the settings. For a list of available settings, see Settings.tsx.

Customisation & branding

Element supports some customisation of the user experience through various branding and theme options. While it doesn't support complete re-branding/private labeling, a more personalised experience can be achieved for your users.

  1. default_theme: Typically either light (the default) or dark, this is the optional name of the colour theme to use. If using custom themes, this can be a theme name from that as well.
  2. default_device_display_name: Optional public name for devices created by login and registration, instead of the default templated string. Note that this option does not support templating, currently.
  3. brand: Optional name for the app. Defaults to Element. This is used throughout the application in various strings/locations.
  4. permalink_prefix: An optional URL pointing to an Element Web deployment. For example, https://app.element.io. This will change all permalinks (via the "Share" menus) to point at the Element Web deployment rather than matrix.to.
  5. desktop_builds: Optional. Where the desktop builds for the application are, if available. This is explained in more detail down below.
  6. mobile_builds: Optional. Like desktop_builds, except for the mobile apps. Also described in more detail down below.
  7. mobile_guide_toast: When true (default), users accessing the Element Web instance from a mobile device will be prompted to download the app instead.
  8. update_base_url: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory containing macos and win32 directories, with the update packages within. Defaults to https://packages.element.io/desktop/update/ in production.
  9. map_style_url: Map tile server style URL for location sharing. e.g. https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE This setting is ignored if your homeserver provides /.well-known/matrix/client in its well-known location, and the JSON file at that location has a key m.tile_server (or the unstable version org.matrix.msc3488.tile_server). In this case, the configuration found in the well-known location is used instead.
  10. welcome_user_id: DEPRECATED An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
  11. custom_translations_url: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of {"affected|translation|key": {"languageCode": "new string"}}. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details.
  12. branding: Options for configuring various assets used within the app. Described in more detail down below.
  13. embedded_pages: Further optional URLs for various assets used within the app. Described in more detail down below.
  14. disable_3pid_login: When false (default), enables the options to log in with email address or phone number. Set to true to hide these options.
  15. disable_login_language_selector: When false (default), enables the language selector on the login pages. Set to true to hide this dropdown.
  16. disable_guests: When false (default), enable guest-related functionality (peeking/previewing rooms, etc) for unregistered users. Set to true to disable this functionality.
  17. user_notice: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time. Takes a configuration object as below:
    1. title: Required. Title to show at the top of the notice.
    2. description: Required. The description to use for the notice.
    3. show_once: Optional. If true then the notice will only be shown once per device.
  18. help_url: The URL to point users to for help with the app, defaults to https://element.io/help.
  19. help_encryption_url: The URL to point users to for help with encryption, defaults to https://element.io/help#encryption.
  20. force_verification: If true, users must verify new logins (eg. with another device / their security key)

desktop_builds and mobile_builds

These two options describe the various availability for the application. When the app needs to promote an alternative download, such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked at to see if the link should be to somewhere else.

Starting with desktop_builds, the following subproperties are available:

  1. available: Required. When true, the desktop app can be downloaded from somewhere.
  2. logo: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
  3. url: Required. The download URL for the app. This is used as a hyperlink.
  4. url_macos: Optional. Direct link to download macOS desktop app.
  5. url_win32: Optional. Direct link to download Windows 32-bit desktop app.
  6. url_win64: Optional. Direct link to download Windows 64-bit desktop app.
  7. url_linux: Optional. Direct link to download Linux desktop app.

When desktop_builds is not specified at all, the app will assume desktop downloads are available from https://element.io

For mobile_builds, the following subproperties are available:

  1. ios: The URL for where to download the iOS app, such as an App Store link. When explicitly null, the app will assume the iOS app cannot be downloaded. When not provided, the default Element app will be assumed available.
  2. android: The same as ios, except for Android instead.
  3. fdroid: The same as android, except for FDroid instead.

Together, these two options might look like the following in your config:

{
    "desktop_builds": {
        "available": true,
        "logo": "https://example.org/assets/logo-small.svg",
        "url": "https://example.org/not_element/download"
    },
    "mobile_builds": {
        "ios": null,
        "android": "https://example.org/not_element/android",
        "fdroid": "https://example.org/not_element/fdroid"
    }
}

branding and embedded_pages

These two options point at various URLs for changing different internal pages (like the welcome page) and logos within the application.

Starting with branding, the following subproperties are available:

  1. welcome_background_url: When a string, the URL for the full-page image background of the login, registration, and welcome pages. This property can additionally be an array to have the app choose an image at random from the selections.
  2. auth_header_logo_url: A URL to the logo used on the login, registration, etc pages.
  3. auth_footer_links: A list of links to add to the footer during login, registration, etc. Each entry must have a text and url property.

embedded_pages can be configured as such:

  1. welcome_url: A URL to an HTML page to show as a welcome page (landing on #/welcome). When not specified, the default welcome.html that ships with Element will be used instead.
  2. home_url: A URL to an HTML page to show within the app as the "home" page. When the app doesn't have a room/screen to show the user, it will use the home page instead. The home page is additionally accessible from the user menu. By default, no home page is set and therefore a hardcoded landing screen is used. More documentation and examples are here.
  3. login_for_welcome: When true (default false), the app will use the login form as a welcome page instead of the welcome page itself. This disables use of welcome_url and all welcome page functionality.

Together, the options might look like this in your config:

{
    "branding": {
        "welcome_background_url": "https://example.org/assets/background.jpg",
        "auth_header_logo_url": "https://example.org/assets/logo.svg",
        "auth_footer_links": [
            { "text": "FAQ", "url": "https://example.org/faq" },
            { "text": "Donate", "url": "https://example.org/donate" }
        ]
    },
    "embedded_pages": {
        "welcome_url": "https://example.org/assets/welcome.html",
        "home_url": "https://example.org/assets/home.html"
    }
}

Note that index.html also has an og:image meta tag that is set to an image hosted on element.io. This is the image used if links to your copy of Element appear in some websites like Facebook, and indeed Element itself. This has to be static in the HTML and an absolute URL (and HTTP rather than HTTPS), so it's not possible for this to be an option in config.json. If you'd like to change it, you can build Element, but run RIOT_OG_IMAGE_URL="http://example.com/logo.png" yarn build. Alternatively, you can edit the og:image meta tag in index.html directly each time you download a new version of Element.

SSO setup

When Element is deployed alongside a homeserver with SSO-only login, some options to ease the user experience might want to be set:

  1. logout_redirect_url: Optional URL to redirect the user to after they have logged out. Some SSO systems support a page that the user can be sent to in order to log them out of that system too, making logout symmetric between Element and the SSO system.
  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 have users which land on the welcome page to be redirected, use "on_welcome_page": true. Additionally, there is an option to redirect anyone landing on the login page, by using "on_login_page": true. As an example:
    {
        "sso_redirect_options": {
            "immediate": false,
            "on_welcome_page": true,
            "on_login_page": true
        }
    }
    
    It is most common to use the immediate flag instead of on_welcome_page.

Native OIDC

Native OIDC support is currently in labs and is subject to change.

Static OIDC Client IDs are preferred and can be specified under oidc_static_clients as a mapping from issuer to configuration object containing client_id. Issuer must have a trailing forward slash. As an example:

{
    "oidc_static_clients": {
        "https://auth.example.com/": {
            "client_id": "example-client-id"
        }
    }
}

If a matching static client is not found, the app will attempt to dynamically register a client using metadata specified under oidc_metadata. The app has sane defaults for the metadata properties below but on stricter policy identity providers they may not pass muster, e.g. contacts may be required. The following subproperties are available:

  1. client_uri: This is the base URI for the OIDC client registration, typically logo_uri, tos_uri, and policy_uri must be either on the same domain or a subdomain of this URI.
  2. logo_uri: Optional URI for the client logo.
  3. tos_uri: Optional URI for the client's terms of service.
  4. policy_uri: Optional URI for the client's privacy policy.
  5. contacts: Optional list of contact emails for the client.

As an example:

{
    "oidc_metadata": {
        "client_uri": "https://example.com",
        "logo_uri": "https://example.com/logo.png",
        "tos_uri": "https://example.com/tos",
        "policy_uri": "https://example.com/policy",
        "contacts": ["support@example.com"]
    }
}

VoIP / Jitsi calls

Currently, Element uses Jitsi to offer conference calls in rooms, with an experimental Element Call implementation in the works. A set of defaults are applied, pointing at our Jitsi and Element Call instances, to ensure conference calling works, however you can point Element at your own if you prefer.

More information about the Jitsi setup can be found here.

The VoIP and Jitsi options are:

  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 was created by an integration manager, or if the homeserver provides Jitsi information in /.well-known/matrix/client. For example:
    {
        "jitsi": {
            "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 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 has a landing page for users to test audio and video before joining the call, otherwise users will automatically join the call. For example:
    {
        "jitsi_widget": {
            "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 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:
    {
        "voip": {
            "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, 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:
    {
        "widget_id": "$arbitrary_string",
        "widget": {
            "creatorUserId": "@user:example.org",
            "id": "$the_same_widget_id",
            "type": "m.custom",
            "waitForIframeLoad": true,
            "name": "My Widget Name Here",
            "avatar_url": "mxc://example.org/abc123",
            "url": "https://example.org/widget.html",
            "data": {
                "title": "Subtitle goes here"
            }
        },
        "layout": {
            "container": "top",
            "index": 0,
            "width": 65,
            "height": 50
        }
    }
    
    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. By default this applies to all rooms, but the behaviour can be skipped for 2-person rooms, causing Element to fall back to 1:1 VoIP, by setting the option widget_build_url_ignore_dm to true.
  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.
  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 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 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 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 used throughout the application in various strings/locations.
    • guest_spa_url: Optional URL for an Element Call single-page app (SPA), for guest links. If this is set, Element Web will expose a "join" link for public video rooms, which can then be shared to non-matrix users. The target Element Call SPA is typically set up to use a homeserver that allows users to register without email ("passwordless guest users") and to federate.

Bug reporting

If you run your own rageshake server to collect bug reports, the following options may be of interest:

  1. bug_report_endpoint_url: URL for where to submit rageshake logs to. Rageshakes include feedback submissions and bug reports. When not present in the config, the app will disable all rageshake functionality. Set to https://element.io/bugreports/submit to submit rageshakes to us, or use your own rageshake server.
  2. uisi_autorageshake_app: If a user has enabled the "automatically send debug logs on decryption errors" flag, this option will be sent alongside the rageshake so the rageshake server can filter them by app name. By default, this will be element-auto-uisi (in contrast to other rageshakes submitted by the app, which use element-web).
  3. existing_issues_url: URL for where to find existing issues.
  4. new_issue_url: URL for where to submit new issues.

If you would like to use Sentry for rageshake data, add a sentry object to your config with the following values:

  1. dsn: The Sentry DSN.
  2. environment: Optional environment to pass to Sentry.

For example:

{
    "sentry": {
        "dsn": "dsn-goes-here",
        "environment": "production"
    }
}

Integration managers

Integration managers are embedded applications within Element to help the user configure bots, bridges, and widgets. An integration manager is a separate piece of software not typically available with your homeserver. To disable integrations, set the options defined here to null.

  1. integrations_ui_url: The UI URL for the integration manager.
  2. integrations_rest_url: The REST interface URL for the integration manager.
  3. integrations_widgets_urls: A list of URLs the integration manager uses to host widgets.

If you would like to use Scalar, the integration manager maintained by Element, the following options would apply:

{
    "integrations_ui_url": "https://scalar.vector.im/",
    "integrations_rest_url": "https://scalar.vector.im/api",
    "integrations_widgets_urls": [
        "https://scalar.vector.im/_matrix/integrations/v1",
        "https://scalar.vector.im/api",
        "https://scalar-staging.vector.im/_matrix/integrations/v1",
        "https://scalar-staging.vector.im/api",
        "https://scalar-staging.riot.im/scalar/api"
    ]
}

For widgets in general (from an integration manager or not) there is also:

  • default_widget_container_height

This controls the height that the top widget panel initially appears as and is the height in pixels, default 280.

Administrative options

If you would like to include a custom message when someone is reporting an event, set the following Markdown-capable field:

{
    "report_event": {
        "admin_message_md": "Please be sure to review our [terms of service](https://example.org/terms) before reporting a message."
    }
}

To add additional "terms and conditions" links throughout the app, use the following template:

{
    "terms_and_conditions_links": [{ "text": "Code of conduct", "url": "https://example.org/code-of-conduct" }]
}

Analytics

To configure Posthog, add the following under posthog in your config:

  1. api_host: The hostname of the posthog server.
  2. project_api_key: The API key from posthog.

When these configuration options are not present, analytics are deemed impossible and the user won't be asked to opt in to the system.

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, 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.

Miscellaneous

Element supports other options which don't quite fit into other sections of this document.

To configure whether presence UI is shown for a given homeserver, set enable_presence_by_hs_url. It is recommended to set this value to the following at a minimum:

{
    "enable_presence_by_hs_url": {
        "https://matrix.org": false,
        "https://matrix-client.matrix.org": false
    }
}

Identity servers

The identity server is used for inviting other users to a room via third party identifiers like emails and phone numbers. It is not used to store your password or account information.

As of Element 1.4.0, all identity server functions are optional and you are prompted to agree to terms before data is sent to the identity server.

Element will check multiple sources when looking for an identity server to use in the following order of preference:

  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 if the user visits Settings and manually changes their identity server.
  2. The identity server provided by the .well-known lookup that occurred at login
  3. The identity server provided by the Riot config file

If none of these sources have an identity server set, then Element will prompt the user to set an identity server first when attempting to use features that require one.

Currently, the only two public identity servers are https://vector.im and https://matrix.org, however in the future identity servers will be decentralised.

Desktop app configuration

See https://github.com/element-hq/element-desktop#user-specified-configjson

UI Features

Parts of the UI can be disabled using UI features. These are settings which appear under setting_defaults and can only be true (default) or false. When false, parts of the UI relating to that feature will be disabled regardless of the user's preferences.

Currently, the following UI feature flags are supported:

  • UIFeature.urlPreviews - Whether URL previews are enabled across the entire application.
  • UIFeature.feedback - Whether prompts to supply feedback are shown.
  • 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.
  • UIFeature.widgets - Whether or not widgets will be shown.
  • UIFeature.advancedSettings - Whether or not sections titled "advanced" in room and user settings are shown to the user.
  • UIFeature.shareQrCode - Whether or not the QR code on the share room/event dialog is shown.
  • UIFeature.shareSocial - Whether or not the social icons on the share room/event dialog are shown.
  • 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 server (sharing email addresses, 3PID invites, etc).
  • 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 not directly related to the identity server.
  • UIFeature.registration - Whether or not the registration page is accessible. Typically useful if accounts are managed externally.
  • UIFeature.passwordReset - Whether or not the password reset page is accessible. Typically useful if accounts are managed externally.
  • UIFeature.deactivate - Whether or not the deactivate account button is accessible. Typically useful if accounts are managed externally.
  • UIFeature.advancedEncryption - Whether or not advanced encryption options 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.
  • UIFeature.TimelineEnableRelativeDates - Display relative date separators (eg: 'Today', 'Yesterday') in the timeline for recent messages. When false day dates will be used.
  • UIFeature.BulkUnverifiedSessionsReminder - Display popup reminders to verify or remove unverified sessions. Defaults to true.
  • UIFeature.locationSharing - Whether or not location sharing menus will be shown.

Undocumented / developer options

The following are undocumented or intended for developer use only.

  1. fallback_hs_url
  2. sync_timeline_limit
  3. dangerously_allow_unsafe_and_insecure_passwords
  4. latex_maths_delims: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when feature_latex_maths is enabled.
  5. voice_broadcast.chunk_length: Target chunk length in seconds for the Voice Broadcast feature currently under development.

Custom Home Page

The home page is shown whenever the user is logged in, but no room is selected. A custom home.html replacing the default home page can be configured either in .well-known/matrix/client or config.json. Such a custom home page can be used to communicate helpful information and important rules to the users.

Configuration

To provide a custom home page for all element-web/desktop users of a homeserver, include the following in .well-known/matrix/client:

{
    "io.element.embedded_pages": {
        "home_url": "https://example.org/home.html"
    }
}

The home page can be overridden in config.json to provide all users of an element-web installation with the same experience:

{
    "embeddedPages": {
        "homeUrl": "https://example.org/home.html"
    }
}

home.html Example

The following is a simple example for a custom home.html:

<style type="text/css">
	.tos {
		width: auto;
		color: black;
		background : #ffcccb;
		font-weight: bold;
	}
</style>

<h1>The example.org Matrix Server</h1>

<div class="tos">
	<p>Behave appropriately.</p>
</div>

<h2>Start Chatting</h2>
<ul>
	<li><a href="#/dm">Send a Direct Message</a></li>
	<li><a href="#/directory">Explore Public Rooms</a></li>
	<li><a href="#/new">Create a Group Chat</a></li>
</ul>

When choosing colors, be aware that the home page may be displayed in either light or dark mode.

It may be needed to set CORS headers for the home.html to enable element-desktop to fetch it, with e.g., the following nginx config:

add_header Access-Control-Allow-Origin *;

Running in Kubernetes

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 Nginx ingress to be installed.

Note that the content of the required config.json is defined inside this yaml because it needs to be put in your Kubernetes cluster as a ConfigMap.

So to use it you must create a file with this content as a starting point and modify it so it meets the requirements of your environment.

Then you can deploy it to your cluster with something like kubectl apply -f my-element-web.yaml.

# This is an example of a POSSIBLE config for deploying a single element-web instance in Kubernetes

# Use the element-web namespace to put it all in.

apiVersion: v1
kind: Namespace
metadata:
  name: element-web

---

# The config.json file is to be put into Kubernetes as a config file in such a way that
# the element web instance can read it.
# The code below shows how this can be done with the config.sample.json content.

apiVersion: v1
kind: ConfigMap
metadata:
  name: element-config
  namespace: element-web
data:
  config.json: |
    {
        "default_server_config": {
            "m.homeserver": {
                "base_url": "https://matrix-client.matrix.org",
                "server_name": "matrix.org"
            },
            "m.identity_server": {
                "base_url": "https://vector.im"
            }
        },
        "disable_custom_urls": false,
        "disable_guests": false,
        "disable_login_language_selector": false,
        "disable_3pid_login": false,
        "brand": "Element",
        "integrations_ui_url": "https://scalar.vector.im/",
        "integrations_rest_url": "https://scalar.vector.im/api",
        "integrations_widgets_urls": [
                "https://scalar.vector.im/_matrix/integrations/v1",
                "https://scalar.vector.im/api",
                "https://scalar-staging.vector.im/_matrix/integrations/v1",
                "https://scalar-staging.vector.im/api",
                "https://scalar-staging.riot.im/scalar/api"
        ],
        "bug_report_endpoint_url": "https://element.io/bugreports/submit",
        "defaultCountryCode": "GB",
        "show_labs_settings": false,
        "features": { },
        "default_federate": true,
        "default_theme": "light",
        "room_directory": {
            "servers": [
                    "matrix.org"
            ]
        },
        "enable_presence_by_hs_url": {
            "https://matrix.org": false,
            "https://matrix-client.matrix.org": false
        },
        "setting_defaults": {
            "breadcrumbs": true
        },
        "jitsi": {
            "preferred_domain": "meet.element.io"
        }
    }


---

# A deployment of the element-web for a single instance

apiVersion: apps/v1
kind: Deployment
metadata:
  name: element
  namespace: element-web
spec:
  selector:
    matchLabels:
      app: element
  replicas: 1
  template:
    metadata:
      labels:
        app: element
    spec:
      containers:
      - name: element
        image: vectorim/element-web:latest
        volumeMounts:
        - name: config-volume
          mountPath: /app/config.json
          subPath: config.json
        ports:
        - containerPort: 80
          name: element
          protocol: TCP
        readinessProbe:
            httpGet:
                path: /
                port: element
            initialDelaySeconds: 2
            periodSeconds: 3
        livenessProbe:
            httpGet:
                path: /
                port: element
            initialDelaySeconds: 10
            periodSeconds: 10
      volumes:
      - name: config-volume
        configMap:
          name: element-config

---

# Wrap it all in a Service

apiVersion: v1
kind: Service
metadata:
  name: element
  namespace: element-web
spec:
  selector:
    app: element
  ports:
    - name: default
      protocol: TCP
      port: 80
      targetPort: 80

---

# An ingress definition to expose the service via a hostname

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: element
  namespace: element-web
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/configuration-snippet: |
      add_header X-Frame-Options SAMEORIGIN;
      add_header X-Content-Type-Options nosniff;
      add_header X-XSS-Protection "1; mode=block";
      add_header Content-Security-Policy "frame-ancestors 'self'";
spec:
  rules:
    - host: element.example.nl
      http:
        paths:
          - pathType: Prefix
            path: /
            backend:
              service:
                name: element
                port:
                  number: 80

---

Jitsi in Element

Element uses Jitsi for conference calls, which provides options for self-hosting your own server and supports most major platforms.

1:1 calls, or calls between you and one other person, do not use Jitsi. Instead, those calls work directly between clients or via TURN servers configured on the respective homeservers.

There's a number of ways to start a Jitsi call: the easiest way is to click on the voice or video buttons near the message composer in a room with more than 2 people. This will add a Jitsi widget which allows anyone in the room to join.

Integration managers (available through the 4 squares in the top right of the room) may provide their own approaches for adding Jitsi widgets.

Configuring Element to use your self-hosted Jitsi server

You can host your own Jitsi server to use with Element. It's usually advisable to use a recent version of Jitsi. In particular, versions older than around 6826 will cause problems with Element 1.9.10 or newer.

Element will use the Jitsi server that is embedded in the widget, even if it is not the one you configured. This is because conference calls must be held on a single Jitsi server and cannot be split over multiple servers.

However, you can configure Element to start a conference with your Jitsi server by adding to your config the following:

{
    "jitsi": {
        "preferred_domain": "your.jitsi.example.org"
    }
}

Element's default is meet.element.io (a free service offered by Element). meet.jit.si is an instance hosted by Jitsi themselves and is also free to use.

Once you've applied the config change, refresh Element and press the call button. This should start a new conference on your Jitsi server.

Note: The widget URL will point to a jitsi.html page hosted by Element. The Jitsi 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 default, and you are using element-web 1.6 or newer, set the following on your homeserver's /.well-known/matrix/client config:

{
    "im.vector.riot.jitsi": {
        "preferredDomain": "your.jitsi.example.org"
    }
}

Element Android

Element Android (1.0.5+) supports custom Jitsi domains, similar to Element Web above.

1:1 calls, or calls between you and one other person, do not use Jitsi. Instead, those calls work directly between clients or via TURN servers configured on the respective homeservers.

For rooms with more than 2 joined members, when creating a Jitsi conference via call/video buttons of the toolbar (not via integration manager), Element Android will create a widget using the wrapper hosted on app.element.io. The domain used is the one specified by the /.well-known/matrix/client endpoint, and if not present it uses the fallback defined in config.json (meet.element.io)

For active Jitsi widgets in the room, a native Jitsi widget UI is created and points to the instance specified in the domain key of the widget content data.

Element Android manages allowed native widgets permissions a bit differently than web widgets (as the data shared are different and never shared with the widget URL). For Jitsi widgets, permissions are requested only once per domain (consent saved in account data).

Jitsi Wrapper

Note: These are developer docs. Please consult your client's documentation for instructions on setting up Jitsi.

The react-sdk wraps all Jitsi call widgets in a local wrapper called jitsi.html which takes several parameters:

Query string:

  • widgetId: The ID of the widget. This is needed for communication back to the react-sdk.
  • parentUrl: The URL of the parent window. This is also needed for communication back to the react-sdk.

Hash/fragment (formatted as a query string):

  • conferenceDomain: The domain to connect Jitsi Meet to.
  • conferenceId: The room or conference ID to connect Jitsi Meet to.
  • isAudioOnly: Boolean for whether this is a voice-only conference. May not be present, should default to false.
  • startWithAudioMuted: Boolean for whether the calls start with audio muted. May not be present.
  • startWithVideoMuted: Boolean for whether the calls start with video muted. May not be present.
  • displayName: The display name of the user viewing the widget. May not be present or could be null.
  • avatarUrl: The HTTP(S) URL for the avatar of the user viewing the widget. May not be present or could be null.
  • userId: The MXID of the user viewing the widget. May not be present or could be null.

The react-sdk will assume that jitsi.html is at the path of wherever it is currently being served. For example, https://develop.element.io/jitsi.html or vector://webapp/jitsi.html.

The jitsi.html wrapper can use the react-sdk's WidgetApi to communicate, making it easier to actually implement the feature.

End to end encryption by default

By default, Element will create encrypted DM rooms if the user you are chatting with has keys uploaded on their account. For private room creation, Element will default to encryption on but give you the choice to opt-out.

Disabling encryption by default

Set the following on your homeserver's /.well-known/matrix/client config:

{
    "io.element.e2ee": {
        "default": false
    }
}

Disabling encryption

Set the following on your homeserver's /.well-known/matrix/client config:

{
    "io.element.e2ee": {
        "force_disable": true
    }
}

When force_disable is true:

  • all rooms will be created with encryption disabled, and it will not be possible to enable encryption from room settings.
  • any io.element.e2ee.default value will be disregarded.

Note: If the server is configured to forcibly enable encryption for some or all rooms, this behaviour will be overridden.

Secure backup

By default, Element strongly encourages (but does not require) users to set up Secure Backup so that cross-signing identity key and message keys can be recovered in case of a disaster where you lose access to all active devices.

Requiring secure backup

To require Secure Backup to be configured before Element can be used, set the following on your homeserver's /.well-known/matrix/client config:

{
    "io.element.e2ee": {
        "secure_backup_required": true
    }
}

Preferring setup methods

By default, Element offers users a choice of a random key or user-chosen passphrase when setting up Secure Backup. If a homeserver admin would like to only offer one of these, you can signal this via the /.well-known/matrix/client config, for example:

{
    "io.element.e2ee": {
        "secure_backup_setup_methods": ["passphrase"]
    }
}

The field secure_backup_setup_methods is an array listing the methods the client should display. Supported values currently include key and passphrase. If the secure_backup_setup_methods field is not present or exists but does not contain any supported methods, Element will fallback to the default value of: ["key", "passphrase"].

Compatibility

The settings above were first proposed under a im.vector.riot.e2ee key, which is now deprecated. Element will check for either key, preferring io.element.e2ee if both exist.

Customisations

🦖 DEPRECATED

Customisations have been deprecated in favour of the Module API. If you have use cases from customisations which are not yet available via the Module API please open an issue. Customisations will be removed from the codebase in a future release.


Element Web and the React SDK support "customisation points" that can be used to easily add custom logic specific to a particular deployment of Element Web.

An example of this is the media customisations module. This module in the React SDK only defines some empty functions and their types: it does not do anything by default.

To make use of these customisation points, you will first need to fork Element Web so that you can add your own code. Even though the default module is part of the React SDK, you can still override it from the Element Web layer:

  1. Copy the default customisation module to element-web/src/customisations/YourNameMedia.ts
  2. Edit customisations points and make sure export the ones you actually want to activate
  3. Create/add an entry to customisations.json next to the webpack config:
{
    "src/customisations/Media.ts": "src/customisations/YourNameMedia.ts"
}

By isolating customisations to their own module, this approach should remove the chance of merge conflicts when updating your fork, and thus simplify ongoing maintenance.

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 default, Element Web does not ship with this file to prevent conflicts.

Custom components

Maintainers can use the above system to override components if they wish. Maintenance and API surface compatibility are left as a responsibility for the project - the layering in Element Web (including the react-sdk) do not make guarantees that properties/state machines won't change.

Component visibility customisation

UI for some actions can be hidden via the ComponentVisibility customisation:

  • inviting users to rooms and spaces,
  • creating rooms,
  • creating spaces,

To customise visibility create a customisation module from ComponentVisibility following the instructions above.

shouldShowComponent determines whether the active MatrixClient user should be able to use the given UI component. When shouldShowComponent returns falsy all UI components for that feature will be hidden. If shown, the user might still not be able to use the component depending on their contextual permissions. For example, invite options might be shown to the user, but they won't have permission to invite users to the current room: the button will appear disabled.

For example, to only allow users who meet a certain condition to create spaces:

function shouldShowComponent(component: UIComponent): boolean {
    if (component === UIComponent.CreateSpaces) {
        // customConditionCheck() is a function of your own creation
        const userMeetsCondition = customConditionCheck(MatrixClientPeg.get().getUserId());
        return userMeetsCondition;
    }
    return true;
}

In this example, all UI related to creating a space will be hidden unless the users meets the custom condition.

Module system

The module system in Element Web is a way to add or modify functionality of Element Web itself, bundled at compile time for the app. This means that modules are loaded as part of the yarn build process but have an effect on user experience at runtime.

Installing modules

If you already have a module you want to install, such as our ILAG Module, then copy build_config.sample.yaml to build_config.yaml in the same directory. In your new build_config.yaml simply add the reference to the module as described by the sample file, using the same syntax you would for yarn add:

modules:
    # Our module happens to be published on NPM, so we use that syntax to reference it.
    - "@vector-im/element-web-ilag-module@latest"

Then build the app as you normally would: yarn build or yarn dist (if compatible on your platform). If you are building the Docker image then ensure your build_config.yaml ends up in the build directory. Usually this works fine if you use the current directory as the build context (the . in docker build -t my-element-web .).

Writing modules

While writing modules is meant to be easy, not everything is possible yet. For modules which want to do something we haven't exposed in the module API, the module API will need to be updated. This means a PR to both this repo and matrix-react-sdk-module-api.

Once your change to the module API is accepted, the @matrix-org/react-sdk-module-api dependency gets updated at the element-web layer (usually by us, the maintainers) to ensure your module can operate.

If you're not adding anything to the module API, or your change was accepted per above, then start off with a clone of our ILAG module which will give you a general idea for what the structure of a module is and how it works.

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).
  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().
  3. The module must be deployed in a way where yarn add can access it, as that is how the build system will try to install it. Note that while this is often NPM, it can also be a GitHub/GitLab repo or private NPM registry. Be careful when using git dependencies in yarn classic, many lifecycle scripts will not be executed which may mean that your module is not built and thus may fail to be imported.

... and that's pretty much it. As with any code, please be responsible and call things in line with the documentation. Both RuntimeModule and ModuleApi have extensive documentation to describe what is proper usage and how to set things up.

If you have any questions then please visit #element-dev:matrix.org on Matrix and we'll help as best we can.

Native Node Modules

This documentation moved to the element-desktop repository.

Choosing an issue to work on

So you want to contribute to Element Web? That is awesome!

If you're not sure where to start, make sure you read CONTRIBUTING.md, and the Development and Setting up a dev environment sections of the README.

Maybe you've got something specific you'd like to work on? If so, make sure you create an issue and discuss it with the developers before you put a lot of time into it.

If you're looking for inspiration on where to start, keep reading!

Finding a good first issue

All the issues for Element Web live in the element-web repository, including issues that actually need fixing in one of the related repos.

The first place to look is for issues tagged with "good first issue".

Look through that list and find something that catches your interest. If there is nothing, there, try gently asking in #element-dev:matrix.org for someone to add something.

When you're looking through the list, here are some things that might make an issue a GOOD choice:

  • It is a problem or feature you care about.
  • It concerns a type of code you know a little about.
  • You think you can understand what's needed.
  • It already has approval from Element Web's designers (look for comments from members of the Product or Design teams).

Here are some things that might make it a BAD choice:

  • 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 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 changes that require approval from one of those teams, you will probably have to wait a very long time. The kind of change affected by this is changing the way the product works, or how it looks in a specific area.

Finding a good second issue

Once you've fixed a few small things, you can consider taking on something a little larger. This should mostly be driven by what you find interesting, but you may also find the Help Wanted label useful.

Note that the same comment applies as in the previous section: if you want to work in areas that require Design or Product approval, you should look to join existing work that is already designed, as getting approval for your random change will take a very long time.

So you should always avoid issues tagged with X-Needs-Design or X-Needs-Product.

Asking questions

Feel free to ask questions about the issues or how to choose them in the #element-dev:matrix.org Matrix room.

Thank you

Thank you again for contributing to Element Web. We welcome your contributions and are grateful for your work. We find working on it great fun, and we hope you do too!

How to translate Element

Requirements

  • Web Browser
  • Be able to understand English
  • Be able to understand the language you want to translate Element into

Join #element-translations:matrix.org

  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
  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 :)

How to check if your language already is being translated

Go to https://localazy.com/p/element-web. If your language is listed then you can get started. Have a read of https://localazy.com/docs/general/translating-strings if you need help getting started. If your language is not yet listed please express your wishes to start translating it in the general discussion room linked above.

What are %(something)s?

These things are placeholders 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 %(count)s as this is also used to determine which pluralisation is used.

These things are markup tags, they encapsulate sections of translations to be marked up, with links, buttons, emphasis and such. You must keep these markers surrounding the equivalent string in your language that needs to be marked up.

When will my translations be available?

We automatically pull changes from Localazy 3 times a week, so your translations should be available at https://develop.element.io within a few days of you submitting them and them being approved. They will then also be included in the following release cycle.

Pull Request Previews

Pull requests to the React SDK layer (and in the future other layers as well) automatically set up a preview site with a full deployment of Element with the changes from the pull request added in so that anyone can easily test and review them. This is especially useful for checking visual and interactive changes.

To access the preview site, click the link in the description of the PR.

The checks section could be collapsed at first, so you may need to click "Show all checks" to reveal them. Look for an entry that mentions deploy-preview. It may be at the end of the list, so you may need scroll a bit to see it. To access the preview site, click the "Details" link in the deploy preview row.

Important: Please always use test accounts when logging into preview sites, as they may contain unreviewed and potentially dangerous code that could damage your account, exfiltrate encryption keys, etc.

FAQs

Are preview sites created for pull requests from contributors?

Yes, they are created for all PRs from any author.

Do preview sites expire after some time period?

No, there is no expiry date, so they should remain accessible indefinitely, but of course they obviously aren't meant to live beyond the development workflow, so please don't rely on them for anything important. They may disappear at any time without notice.

Review Guidelines

The following summarises review guidelines that we follow for pull requests in Element Web and other supporting repos. These are just guidelines (not strict rules) and may be updated over time.

Code Review

When reviewing code, here are some things we look for and also things we avoid:

We review for

  • Correctness
  • Performance
  • Accessibility
  • Security
  • Quality via automated and manual testing
  • Comments and documentation where needed
  • Sharing knowledge of different areas among the team
  • Ensuring it's something we're comfortable maintaining for the long term
  • Progress indicators and local echo where appropriate with network activity

We should avoid

  • Style nits that are already handled by the linter
  • Dramatically increasing scope

Good practices

  • Use empathetic language
  • Authors should prefer smaller commits for easier reviewing and bisection
  • Reviewers should be explicit about required versus optional changes
    • Reviews are conversations and the PR author should feel comfortable discussing and pushing back on changes before making them
  • Reviewers are encouraged to ask for tests where they believe it is reasonable
  • Core team should lead by example through their tone and language
  • 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 towards making others feel like colleagues working towards a common goal

Workflow

  • 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 be more appropriate)
  • Reviewers should remove the team review request and request review from themselves when starting a review to avoid double review
  • If there are multiple related PRs authors should reference each of the PRs in the others before requesting review. Reviewers might start reviewing from different places and could miss other required PRs.
  • Avoid force pushing to a PR after the first round of review
  • Use the GitHub default of merge commits when landing (avoid alternate options like squash or rebase)
  • PR author merges after review (assuming they have write access)
  • Assign issues only when in progress to indicate to others what can be picked up

Code Quality

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 like to change that.

  • For new features, code reviewers will expect some form of automated testing to be included by default
  • 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

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 writing a test when making your next change. As a reviewer, try to think about how you might test the area of code you are reviewing. If the reviewer agrees it would be quite difficult to test some new feature, then it's okay for them to accept the change without tests for now, but we'd eventually like to be more strict about this further down the road.

If you do spot areas that are quite hard to test today, please let us know in #element-dev:matrix.org. We can work on improving the app architecture and testing helpers so that future tests are easier for everyone to write, but we won't know which parts are difficult unless people shout when stumbling through them.

We recognise that this testing policy will slow things down a bit, but overall it should encourage better long-term health of the app and give everyone more confidence when making changes as coverage increases over time.

For changes guarded by a feature flag, we currently lean towards prioritising our ability to evolve quickly using such flags and thus we will not currently require tests to appear at the same time as the initial landing of features guarded by flags, as long as (for new flagged features going forward) the feature author understands that they are effectively deferring part of their work (adding tests) until later and tests are expected to appear before the feature can be enabled by default.

Design and Product Review

We want to ensure that all changes to Element fit with our design and product vision. We often request review from those teams so they can provide their perspective.

In more detail, our usual process for changes that affect the UI or alter user functionality is:

  • For changes that will go live when merged, always flag Design and Product teams as appropriate
  • For changes guarded by a feature flag, Design and Product review is not 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 files in a PR, a preview site that includes your changes will be added automatically so that anyone who's interested can try them out easily.

Before starting work on a feature, it's best to ensure your plan aligns well with our vision for Element. Please chat with the team in #element-dev:matrix.org before you start so we can ensure it's something we'd be willing to merge.

App load order

Dev note: As of March 2022, the skin is no longer part of the app load order at all. The document's graphs have been kept untouched for posterity.

Old slow flow:

flowchart TD
    A1(((load_modernizr))) --> B
    A2((rageshake)) --> B
    B(((skin))) --> C
    C(((olm))) --> D
    D{mobile} --> E
    E((config)) --> F
    F((i18n)) --> G
    style F stroke:lime
    G(((theme))) --> H
    H(((modernizr))) --> app
    style H stroke:red

Current more parallel flow:

flowchart TD
    subgraph index.ts
        style index.ts stroke:orange

        A[/rageshake/] --> B{mobile}
        B-- No -->C1(.)
        B-- Yes -->C2((redirect))
        C1 --> D[/olm/] --> R
        C1 --> E[platform] --> F[/config/]
        F --> G1[/skin/]
        F --> R
        G1 --> H
        G1 --> R
        F --> G2[/theme/]
        G2 --> H
        G2 --> R
        F --> G3[/i18n/]
        G3 --> H
        G3 --> R
        H{modernizr}-- No --> J((incompatible))-- user ignore --> R
        H-- Yes --> R

        linkStyle 0,7,9,11,12,14,15 stroke:blue;
        linkStyle 4,8,10,13,16 stroke:red;
    end

    R>ready] --> 2A
    style R stroke:gray

    subgraph init.tsx
        style init.tsx stroke:lime
        2A[loadApp] --> 2B[matrixchat]
    end

Key:

  • Parallelogram: async/await task
  • Box: sync task
  • Diamond: conditional branch
  • Circle: user interaction
  • Blue arrow: async task is allowed to settle but allowed to fail
  • Red arrow: async task success is asserted

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).
  • 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:

flowchart TD
    A((rageshake))
    B{mobile}
    C((config))
    D(((olm)))
    E((i18n))
    F(((load_modernizr)))
    G(((modernizr)))
    H(((skin)))
    I(((theme)))
    X[app]

    A --> G
    A --> B
    A-- assert -->X
    F --> G --> X
    G --> H --> X
    C --> I --> X
    C --> E --> X
    E --> G
    B --> C-- assert -->X
    B --> D --> X

    style X stroke:red
    style G stroke:red
    style E stroke:lime
    linkStyle 0,11 stroke:yellow;
    linkStyle 2,13 stroke:red;

How to translate Element (Dev Guide)

Requirements

  • A working Development Setup
  • Latest LTS version of Node.js installed
  • Be able to understand English

Translating strings vs. marking strings for translation

Translating strings are done with the _t() function found in languageHandler.tsx. It is recommended to call this function wherever you introduce a string constant which should be translated. However, translating can not be performed until after the translation system has been initialized. Thus, sometimes translation must be performed at a different location in the source code than where the string is introduced. This breaks some tooling and makes it difficult to find translatable strings. Therefore, there is the alternative _td() function which is used to mark strings for translation, without actually performing the translation (which must still be performed separately, and after the translation system has been initialized).

Basically, whenever a translatable string is introduced, you should call either _t() immediately OR _td() and later _t().

Example:

// Module-level constant
const COLORS = {
    '#f8481c': _td('reddish orange'), // Can't call _t() here yet
    '#fc2647': _td('pinky red') // Use _td() instead so the text is picked up for translation anyway
}

// Function that is called some time after i18n has been loaded
function getColorName(hex) {
    return _t(COLORS[hex]); // Perform actual translation here
}

Key naming rules

These rules are based on https://github.com/element-hq/element-x-android/blob/develop/tools/localazy/README.md At this time we are not trying to have a translation key per UI element as some methodologies use, whilst that would offer the greatest flexibility, it would also make reuse between projects nigh impossible. We are aiming for a set of common strings to be shared then some more localised translations per context they may appear in.

  1. Ensure the string doesn't already exist in a related project, such as https://localazy.com/p/element
  2. Keys for common strings, i.e. strings that can be used at multiple places must start by action_ if this is a verb, or common_ if not
  3. Keys for common accessibility strings must start by a11y_. Example: a11y_hide_password
  4. Otherwise, try to group keys logically and nest where appropriate, such as keyboard_ for strings relating to keyboard shortcuts.
  5. Ensure your translation keys do not include . or | or . Try to balance string length against descriptiveness.

Adding new strings

  1. Check if the import import { _t } from ".../languageHandler"; is present. If not add it to the other import statements. Also import _td if needed.
  2. Add _t() to your string passing the translation key you come up with based on the rules above. 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.
  3. Run yarn i18n to add the keys to src/i18n/strings/en_EN.json
  4. Modify the new entries in src/i18n/strings/en_EN.json with the English (UK) translations for the added keys.

Editing existing strings

Edits to existing strings should be performed only via Localazy. There you can also require all translations to be redone if the meaning of the string has changed significantly.

Adding variables inside a string.

  1. Extend your _t() call. Instead of _t(TKEY) use _t(TKEY, {})
  2. 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.
  3. Add it to the array in _t for example _t(TKEY, {variable: this.variable})
  4. 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 preferred 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 '' 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> }).

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.
  • 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.
  • 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.
  • 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 repetition, 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.
  • Don't forget curly braces when you assign an expression to JSX attributes in the render method)

Theming Element

Themes are a very basic way of providing simple alternative look & feels to the Element app via CSS & custom imagery.

To define a theme for Element:

  1. Pick a name, e.g. teal. at time of writing we have light and dark.
  2. Fork res/themes/dark/css/dark.pcss to be teal.pcss
  3. Fork res/themes/dark/css/_base.pcss to be _teal.pcss
  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 overridden. If every single colour is being changed (as per _dark.pcss) then you might as well keep them all.
  5. Add the theme to the list of entrypoints in webpack.config.js
  6. Add the theme to the list of themes in theme.ts
  7. Sit back and admire your handywork.

In future, the assets for a theme will probably be gathered together into a single directory tree.

Custom Themes

Themes derived from the built in themes may also be defined in settings.

To avoid name collisions, the internal name of a theme is custom-${theme.name}. So if you want to set the custom theme below as the default theme, you would use default_theme: "custom-Electric Blue".

e.g. in config.json:

"setting_defaults": {
        "custom_themes": [
            {
                "name": "Electric Blue",
                "is_dark": false,
                "fonts": {
                    "faces": [
                        {
                            "font-family": "Inter",
                            "src": [{"url": "/fonts/Inter.ttf", "format": "ttf"}]
                        }
                    ],
                    "general": "Inter, sans",
                    "monospace": "'Courier New'"
                },
                "colors": {
                    "accent-color": "#3596fc",
                    "primary-color": "#368bd6",
                    "warning-color": "#ff4b55",
                    "sidebar-color": "#27303a",
                    "roomlist-background-color": "#f3f8fd",
                    "roomlist-text-color": "#2e2f32",
                    "roomlist-text-secondary-color": "#61708b",
                    "roomlist-highlights-color": "#ffffff",
                    "roomlist-separator-color": "#e3e8f0",
                    "timeline-background-color": "#ffffff",
                    "timeline-text-color": "#2e2f32",
                    "timeline-text-secondary-color": "#61708b",
                    "timeline-highlights-color": "#f3f8fd",

                    // These should both be 8 values long
                    "username-colors": ["#ff0000", /*...*/],
                    "avatar-background-colors": ["#cc0000", /*...*/]
                },
                "compound": {
                    "--cpd-color-icon-accent-tertiary": "var(--cpd-color-blue-800)",
                    "--cpd-color-text-action-accent": "var(--cpd-color-blue-900)"
                }
            }, {
                "name": "Deep Purple",
                "is_dark": true,
                "colors": {
                    "accent-color": "#6503b3",
                    "primary-color": "#368bd6",
                    "warning-color": "#b30356",
                    "sidebar-color": "#15171B",
                    "roomlist-background-color": "#22262E",
                    "roomlist-text-color": "#A1B2D1",
                    "roomlist-text-secondary-color": "#EDF3FF",
                    "roomlist-highlights-color": "#343A46",
                    "roomlist-separator-color": "#a1b2d1",
                    "timeline-background-color": "#181b21",
                    "timeline-text-color": "#EDF3FF",
                    "timeline-text-secondary-color": "#A1B2D1",
                    "timeline-highlights-color": "#22262E"
                }
            }
        ]
    }

compound may contain overrides for any semantic design token belonging to our design system. The above example shows how you might change the accent color to blue by setting the relevant semantic tokens to refer to blue base tokens.

All properties in fonts are optional, and will default to the standard Riot fonts.

Playwright in Element Web

Contents

  • How to run the tests
  • How the tests work
  • How to write great Playwright tests
  • Visual testing

Running the Tests

Our Playwright tests run automatically as part of our CI along with our other tests, on every pull request and on every merge to develop & master.

You may need to follow instructions to set up your development environment for running Playwright by following https://playwright.dev/docs/browsers#install-browsers and https://playwright.dev/docs/browsers#install-system-dependencies.

However the Playwright tests are run, an element-web instance must be running on http://localhost:8080 (this is configured in playwright.config.ts) - this is what will be tested. When running Playwright tests yourself, the standard yarn start from the element-web project is fine: leave it running it a different terminal as you would when developing. Alternatively if you followed the development set up from element-web then Playwright will be capable of running the webserver on its own if it isn't already running.

The tests use Docker to launch Homeserver (Synapse or Dendrite) instances to test against, so you'll also need to have Docker installed and working in order to run the Playwright tests.

There are a few different ways to run the tests yourself. The simplest is to run:

docker pull ghcr.io/element-hq/synapse:develop
yarn run test:playwright

This will run the Playwright tests once, non-interactively.

Note: you don't need to run the docker pull command every time, but you should do it regularly to ensure you are running against an up-to-date Synapse.

You can also run individual tests this way too, as you'd expect:

yarn run test:playwright --spec playwright/e2e/register/register.spec.ts

Playwright also has its own UI that you can use to run and debug the tests. To launch it:

yarn run test:playwright:open --headed --debug

See more command line options at https://playwright.dev/docs/test-cli.

Running with Rust cryptography

matrix-js-sdk is currently in the process of being updated to replace its end-to-end encryption implementation to use the Matrix Rust SDK. This is not currently enabled by default, but it is possible to have Playwright configure Element to use the Rust crypto implementation by passing --project="Rust Crypto" or using the top left options in open mode.

How the Tests Work

Everything Playwright-related lives in the playwright/ subdirectory of react-sdk as is typical for Playwright tests. Likewise, tests live in playwright/e2e.

playwright/plugins/homeservers contains Playwright plugins that starts instances of Synapse/Dendrite in Docker containers. These servers are what Element-web runs against in the tests.

Synapse can be launched with different configurations in order to test element in different configurations. playwright/plugins/homeserver/synapse/templates contains template configuration files for each different configuration.

Each test suite can then launch whatever Synapse instances it needs in whatever configurations.

Note that although tests should stop the Homeserver instances after running and the plugin also stop any remaining instances after all tests have run, it is possible to be left with some stray containers if, for example, you terminate a test such that the after() does not run and also exit Playwright uncleanly. All the containers it starts are prefixed, so they are easy to recognise. They can be removed safely.

After each test run, logs from the Synapse instances are saved in playwright/logs/synapse with each instance in a separate directory named after its ID. These logs are removed at the start of each test run.

Writing Tests

Mostly this is the same advice as for writing any other Playwright test: the Playwright docs are well worth a read if you're not already familiar with Playwright testing, eg. https://playwright.dev/docs/best-practices. To avoid your tests being flaky it is also recommended to use auto-retrying assertions.

Getting a Synapse

We heavily leverage the magic of Playwright fixtures. To acquire a homeserver within a test just add the homeserver fixture to the test:

test("should do something", async ({ homeserver }) => {
    // homeserver is a Synapse/Dendrite instance
});

This returns an object with information about the Homeserver instance, including what port it was started on and the ID that needs to be passed to shut it down again. It also returns the registration shared secret (registrationSecret) that can be used to register users via the REST API. The Homeserver has been ensured ready to go by awaiting its internal health-check.

Homeserver instances should be reasonably cheap to start (you may see the first one take a while as it pulls the Docker image). You do not need to explicitly clean up the instance as it will be cleaned up by the fixture.

Synapse Config Templates

When a Synapse instance is started, it's given a config generated from one of the config templates in playwright/plugins/homeserver/synapse/templates. There are a couple of special files in these templates:

  • homeserver.yaml: Template substitution happens in this file. Template variables are:
    • REGISTRATION_SECRET: The secret used to register users via the REST API.
    • MACAROON_SECRET_KEY: Generated each time for security
    • FORM_SECRET: Generated each time for security
    • PUBLIC_BASEURL: The localhost url + port combination the synapse is accessible at
  • localhost.signing.key: A signing key is auto-generated and saved to this file. Config templates should not contain a signing key and instead assume that one will exist in this file.

All other files in the template are copied recursively to /data/, so the file foo.html in a template can be referenced in the config as /data/foo.html.

Logging In

We again heavily leverage the magic of Playwright fixtures. To acquire a logged-in user within a test just add the user fixture to the test:

test("should do something", async ({ user }) => {
    // user is a logged in user
});

You can specify a display name for the user via test.use displayName, otherwise a random one will be generated. This will register a random userId using the registrationSecret with a random password and the given display name. The user fixture will contain details about the credentials for if they are needed for User-Interactive Auth or similar but localStorage will already be seeded with them and the app loaded (path /).

Joining a Room

Many tests will also want to start with the client in a room, ready to send & receive messages. Best way to do this may be to get an access token for the user and use this to create a room with the REST API before logging the user in. You can make use of the bot fixture and the client field on the app fixture to do this.

Try to write tests from the users' perspective

Like for instance a user will not look for a button by querying a CSS selector. Instead, you should work with roles / labels etc, see https://playwright.dev/docs/locators.

Using matrix-js-sdk

Due to the way we run the Playwright tests in CI, at this time you can only use the matrix-js-sdk module exposed on window.matrixcs. This has the limitation that it is only accessible with the app loaded. This may be revisited in the future.

Good Test Hygiene

This section mostly summarises general good Playwright testing practice, and should not be news to anyone already familiar with Playwright.

  1. Test a well-isolated unit of functionality. The more specific, the easier it will be to tell what's wrong when they fail.
  2. Don't depend on state from other tests: any given test should be able to run in isolation.
  3. Try to avoid driving the UI for anything other than the UI you're trying to test. e.g. if you're testing that the user can send a reaction to a message, it's best to send a message using a REST API, then react to it using the UI, rather than using the element-web UI to send the message.
  4. Avoid explicit waits. Playwright locators & assertions will implicitly wait for the specified element to appear and all assertions are retried until they either pass or time out, so you should never need to manually wait for an element.
    • For example, for asserting about editing an already-edited message, you can't wait for the 'edited' element to appear as there was already one there, but you can assert that the body of the message is what is should be after the second edit and this assertion will pass once it becomes true. You can then assert that the 'edited' element is still in the DOM.
    • You can also wait for other things like network requests in the browser to complete (https://playwright.dev/docs/api/class-page#page-wait-for-response). Needing to wait for things can also be because of race conditions in the app itself, which ideally shouldn't be there!

This is a small selection - the Playwright best practices guide, linked above, has more good advice, and we should generally try to adhere to them.

Screenshot testing

When we previously used Cypress we also dabbled with Percy, and whilst powerful it did not lend itself well to being executed on all PRs without needing to budget it substantially.

Playwright has built-in support for visual comparison testing. Screenshots are saved in playwright/snapshots and are rendered in a Linux Docker environment for stability.

One must be careful to exclude any dynamic content from the screenshot, such as timestamps, avatars, etc, via the mask option. See the Playwright docs.

Some UI elements render differently between test runs, such as BaseAvatar when there is no avatar set, choosing a colour from the theme palette based on the hash of the user/room's Matrix ID. To avoid this creating flaky tests we inject some custom CSS, for this to happen we use the custom assertion toMatchScreenshot instead of the native toHaveScreenshot.

If you are running Linux and are unfortunate that the screenshots are not rendering identically, you may wish to specify --ignore-snapshots and rely on Docker to render them for you.

Memory leaks

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 is going wrong.

Common symptoms are clicking on a room and it feels like the tab froze and scrolling becoming jumpy/staggered.

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 this happens though so we can try and narrow down what might have gone wrong.

Memory profiles/snapshots

When investigating memory leaks/problems it's usually important to compare snapshots from different points in the Element session lifecycle. Most importantly, a snapshot to establish the baseline or "normal" memory usage is useful. Taking a snapshot roughly 30-60 minutes after starting Element is a good time to establish "normal" memory usage for the app - anything after that is at risk of hiding the memory leak and anything newer is still in the warmup stages of the app.

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 send them over an encrypted private message. Do not share your memory profile in public channels or with people you do not trust.

Taking a memory profile (Firefox)

  1. Press CTRL+SHIFT+I (I as in eye).
  2. Click the Memory tab.
  3. Press the camera icon in the top left of the pane.
  4. Wait a bit (coffee is a good option).
  5. When the save button appears on the left side of the panel, click it to save the profile locally.
  6. Compress the file (gzip or regular zip) to make the file smaller.
  7. Send the compressed file to whoever asked for it (if you trust them).

While the profile is in progress, the tab might be frozen or unresponsive.

Taking a memory profile (Chrome/Desktop)

  1. Press CTRL+SHIFT+I (I as in eye).
  2. Click the Memory tab.
  3. Select "Heap Snapshot" and the app.element.io VM instance (not the indexeddb one).
  4. Click "Take Snapshot".
  5. Wait a bit (coffee is a good option).
  6. When the save button appears on the left side of the panel, click it to save the profile locally.
  7. Compress the file (gzip or regular zip) to make the file smaller.
  8. Send the compressed file to whoever asked for it (if you trust them).

While the profile is in progress, the tab might be frozen or unresponsive.

Jitsi wrapper developer docs

If you're looking for information on how to set up Jitsi in your Element, see jitsi.md instead.

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.

Brief introduction to widgets

Widgets are embedded web applications in a room, controlled through state events, and have a url property. They are largely specified by MSC1236 and have extensions proposed under MSC1286.

The url is typically something we shove into an iframe with sandboxing (see AppTile in the react-sdk), though for some widgets special integration can be done. v2 widgets have a data object which helps achieve that special integration, though v1 widgets are best iframed and left alone.

Widgets have a postMessage API they can use to interact with Element, which also allows Element to interact with them. Typically this is most used by the sticker picker (an account-level widget), though widgets like the Jitsi widget will request permissions to get 'stuck' into the room list during a conference.

Widgets can be added with the /addwidget <url> command.

Brief introduction to integration managers

Integration managers (like Scalar and Dimension) are accessible via the 4 squares in the top right of the room and provide a simple UI over top of bridges, bots, and other stuff to plug into a room. They are a separate service to Element and are thus iframed in a dialog as well. They also have a postMessage API they can use to interact with the client to create things like widgets, give permissions to bridges, and generally set everything up for the integration the user is working with.

Integration managers do not currently have a spec associated with them, though efforts are underway in MSC1286.

Widgets configured by integration managers

Integration managers will often "wrap" a widget by using a widget url which points to the integration manager instead of to where the user requested the widget be. For example, a custom widget added in an integration manager for https://matrix.org will end up creating a widget with a URL like https://integrations.example.org?widgetUrl=https%3A%2F%2Fmatrix.org.

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 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 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 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 therefore have no connection back to Riot.

Jitsi widgets from integration managers

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 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 server they specified in their config.json - this is expected.

Some integration managers allow the user to change the conference name while others will generate one for the user.

Jitsi widgets generated by Element itself

When the user clicks on the call buttons by the composer, the integration manager is not involved in the slightest. Instead, Element itself generates a widget event, this time using the config.json parameters, and publishes that to the room. If there's only two people in the room, a plain WebRTC call is made instead of using a widget at all - these are defined in the Matrix specification.

The Jitsi widget created by Element uses a local jitsi.html wrapper (or one hosted by https://app.element.io for desktop users or those on non-https domains) as the widget url. The wrapper has some basic functionality for talking to Element to ensure the required postMessage calls are fulfilled.

Note: Per jitsi.md the preferredDomain can also come from the server's client .well-known data.

The Jitsi wrapper in Element

Whenever Element sees a Jitsi widget, it ditches the url and instead replaces it with its local wrapper, much like what it would do when creating a widget. However, instead of using one from app.element.io, it will use one local to the client instead.

The wrapper is used to provide a consistent experience to users, as well as being faster and less risky to load. The local wrapper URL is populated with the conference information from the original widget (which could be a v1 or v2 widget) so the user joins the right call.

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 conference servers and therefore different calls entirely.

Note: Per jitsi.md the preferredDomain can also come from the server's client .well-known data.

Feature flags

When developing new features for Element, we use feature flags to give us more flexibility and control over when and where those features are enabled.

For example, flags make the following things possible:

  • Extended testing of a feature via labs on develop
  • 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 Element instance)

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. A large project might use several feature flags if it's useful to control the deployment of different portions independently.

Everyone involved in a feature (engineering, design, product, reviewers) should think about its deployment plan up front as best as possible so we can have the right feature flags in place from the start.

Interaction with spec process

Historically, we have often used feature flags to guard client features that depend on unstable spec features. Unfortunately, there was never clear agreement about how long such a flag should live for, when it should be removed, etc.

Under the new spec process, server-side unstable features can be used by clients and enabled by default as long as clients commit to doing the associated clean up work once a feature stabilises.

Starting work on a feature

When starting work on a feature, we should create a matching feature flag:

  1. Add a new setting of the form:
    "feature_cats": {
        isFeature: true,
        displayName: _td("Adds cats everywhere"),
        supportedLevels: LEVELS_FEATURE,
        default: false,
    },
  1. Check whether the feature is enabled as appropriate:
SettingsStore.getValue("feature_cats");
  1. Document the feature in the labs documentation

With these steps completed, the feature is disabled by default, but can be enabled on develop and nightly by interested users for testing.

Different features may have different deployment plans for when to enable where. The following lists a few common options.

Enabling by default on develop and nightly

Set the feature to true in the develop and nightly configs:

    "features": {
        "feature_cats": true
    },

Enabling by default on staging, app, and release

Set the feature to true in the staging / app and release configs.

Note: The above will only enable the feature for https://app.element.io and official Element Desktop builds. It will not be enabled for self-hosted installed, custom desktop builds, etc. To cover these cases, change the setting's default in Settings.tsx to true.

Feature deployed successfully

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:

  1. Remove isFeature from the setting
  2. Change the default to true (if desired).
  3. Remove the feature from the labs documentation
  4. Celebrate! 🥳

If the feature is meant to be forced on (non-configurable):

  1. Remove the setting
  2. Remove all getValue lines that test for the feature.
  3. Remove the feature from the labs documentation
  4. If applicable, remove the feature state from develop, nightly, staging / app, and release configs
  5. Celebrate! 🥳

OIDC and delegated authentication

Compatibility/OIDC-aware mode

MSC2965: OIDC provider discovery MSC3824: OIDC aware clients This mode uses an SSO flow to gain a loginToken from the authentication provider, then continues with SSO login. Element Web uses MSC2965: OIDC provider discovery to discover the configured provider. Wherever valid MSC2965 configuration is discovered, OIDC-aware login flow will be the only option offered.

(🧪Experimental) OIDC-native flow

Can be enabled by a config-level-only setting in config.json

{
    "features": {
        "feature_oidc_native_flow": true
    }
}

See https://areweoidcyet.com/client-implementation-guide/ for implementation details.

Element Web uses MSC2965: OIDC provider discovery to discover the configured provider. Where OIDC native login flow is enabled and valid MSC2965 configuration is discovered, OIDC native login flow will be the only login option offered. Element Web will attempt to dynamically register with the configured OP. Then, authentication will be completed as described here.

Statically configured OIDC clients

Clients that are already registered with the OP can configure their client_id in config.json. Where static configuration exists for the OP dynamic client registration will not be attempted.

{
    "oidc_static_clients": {
        "https://dummyoidcprovider.com/": {
            "client_id": "abc123"
        }
    }
}

Tip: Paste this into the browser console to make the checkboxes on this page tickable. (Bear in mind that your ticks will be lost if you reload though.)

document.querySelectorAll("input[type='checkbox']").forEach(i => {i.disabled = false;})

Branches

develop

The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable. It corresponds to the develop.element.io CD platform.

staging

The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.

master

The master branch is the most stable as it is the very latest non-RC release. Deployed to app.element.io manually.

Versions

The matrix-js-sdk follows semver, most releases will bump the minor version number. Breaking changes will bump the major version number. Element Web & Element Desktop do not follow semver and always have matching version numbers. The patch version number is normally incremented for every release.

Release Types

Release candidate

A normal release begins with a Release Candidate on the Tick phase of the release cycle, and may contain as many further RCs as are needed before the Tock phase of cycle. Each subsequent RC may add additional commits via any of the means of preparation.

A normal release is the most typical run-of-the-mill release, with at least one RC (Release Candidate) followed by a FINAL release. The typical cadence for these is every 2 weeks we'll do a new initial RC, then the following week we'll do that release cycle's FINAL release with sometimes more RCs in between, as needed.

Final

A normal release culminates with a Final release on the Tock phase of the cycle. This may be merely shipping the very latest RC with an adjusted version number, but can also include (hopefully small) additional changes present on staging if they are deemed safe to skip an RC.

Hotfix / Security

This is an accelerated type of release which sits somewhere between RC and Final. They tend to contain few patches delta from the previous release but also skip any form of RC and in the case of Security the patch lands on GitHub only moments prior. For all intents and purposes they are the same as a Final release but with a different purpose.

Release Blockers

You should become release rabbit on the day after the last full release. For that week, it's your job to keep an eye on the Releases room and see whether any issues marked X-Release-Blocker are opened, or were already open. You should chase people to fix them, so that on RC day you can make the release.

If release-blocking issues are still open, you need to delay the release until they are fixed or reclassified.

There are two labels for tracking release blockers.

X-Release-Blocker

This label applied to an issue means we cannot ship a release affected by the specific issue. This means we cannot cut branches for an RC but security & hotfix releases may still be fine.

X-Upcoming-Release-Blocker

This label applied to an issue means that the next (read: not current) release cycle will be affected by the specific issue. This label will automagically convert to X-Release-Blocker at the conclusion of a full release.

Repositories

This release process revolves around our main repositories:

We own other repositories, but they have more ad-hoc releases and are not part of the bi-weekly cycle:

  • https://github.com/matrix-org/matrix-web-i18n/
  • https://github.com/matrix-org/matrix-react-sdk-module-api

Prerequisites

  • You must be part of the 2 Releasers GitHub groups:
  • You will need access to the VPN (docs) to be able to follow the instructions under Deploy below.
  • You will need the ability to SSH in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for:
    • Two-factor authentication to be set up on your SSH key. (This is needed to get access to production).
    • SSH access to horme (staging.element.io and app.element.io)
    • Permission to sudo on horme as the user element
  • You need "jumphost" configuration in your local ~/.ssh/config. This should have been set up as part of your onboarding.

Overview

flowchart TD
    P[[Prepare staging branches]]
    P --> R1

    subgraph Releasing
        R1[[Releasing matrix-js-sdk]]
        R2[[Releasing element-web]]
        R3[[Releasing element-desktop]]

        R1 --> R2 --> R3
    end

    R3 --> D1

    subgraph Deploying
        D1[\Deploy staging.element.io/]
        D2[\Check dockerhub/]
        D3[\Deploy app.element.io/]
        D4[\Check desktop package/]

        D1 --> D2 --> D
        D{FINAL?}
        D -->|Yes| D3 --> D4
    end

    D -->|No| H1
    D4 --> H1

    subgraph Housekeeping
        H1[\Update topics/]
        H2[\Announce/]
        H3[\Archive done column/]
        H4[\Add diary entry/]
        H5[\Renovate/]

        H1 --> H2 --> H

        H{FINAL?}
        H -->|Yes| H3 --> H4 --> DONE
        H -->|No| H5
    end

    DONE([You are done!])
    H5 --> DONE

Preparation

The goal of this stage is to get the code you want to ship onto the staging branch. There are multiple ways to accomplish this depending on the type of release you need to perform.

For the first RC in a given release cycle the easiest way to prepare branches is using the Cut branches automation - this will take develop and merge it into the staging on the chosen repositories.

For subsequent RCs, if you need to include a change you may PR it directly to the staging branch or rely on the backport automation via labelling a PR to develop with backport staging which will cause a new PR to be opened which backports the requested change to the staging branch.

For security, you may wish to merge the security advisory private fork or apply the patches manually and then push them directly to staging. It is worth noting that at the end of the Final/Hotfix/Security release staging is merged to master which is merged back into develop - this means that any commit which goes to staging will eventually make its way back to the default branch.

  • The staging branch is prepared

Releasing

Shortly after concluding the preparation stage (or pushing any changes to staging in general); a draft release will be automatically made on the 4 project repositories with suggested changelogs and version numbers.

Note: we should add a step here to write summaries atop the changelogs manually, or via AI

Publishing the SDKs to npm also commits a dependency upgrade to the relevant downstream projects, if you skip a layer of this release (e.g. for a hotfix) then the dependency will remain on #develop which will be switched back to the version of the dependency from the master branch to not leak develop code into a release.

Matrix JS SDK

  • Check the draft release which has been generated by the automation
  • Make any changes to the release notes in the draft release as are necessary - Do not click publish, only save draft
  • Kick off a release using the automation - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.

Element Web

  • Check the draft release which has been generated by the automation
  • Make any changes to the release notes in the draft release as are necessary - Do not click publish, only save draft
  • Kick off a release using the automation - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.

Element Desktop

  • Check the draft release which has been generated by the automation
  • Make any changes to the release notes in the draft release as are necessary - Do not click publish, only save draft
  • Kick off a release using the automation - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.

Deploying

We ship the SDKs to npm, this happens as part of the release process. We ship Element Web to dockerhub, *.element.io, and packages.element.io. We ship Element Desktop to packages.element.io.

  • Check that element-web has shipped to dockerhub
  • Deploy staging.element.io. See docs.
  • Test staging.element.io

For final releases additionally do these steps:

  • Deploy app.element.io. See docs.
  • Test app.element.io
  • Ensure Element Web package has shipped to packages.element.io
  • Ensure Element Desktop packages have shipped to packages.element.io

Housekeeping

We have some manual housekeeping to do in order to prepare for the next release.

(show)

With wording like:

Element Web v1.11.24 is here!

This version adds ... and fixes bugs ...

Check it out at app.element.io, in Element Desktop, or from Docker Hub. Changelog and more details at https://github.com/element-hq/element-web/releases/tag/v1.11.24

For the first RC of a given release cycle do these steps:

For final releases additionally do these steps:

  • Archive done column on the team board Note: this should be automated
  • Add entry to the milestones diary. The document says only to add significant releases, but we add all of them just in case.

Skinning

Skinning in the context of the react-sdk is component replacement rather than CSS. This means you can override (replace) any accessible component in the project to implement custom behaviour, look & feel, etc. Depending on your approach, overriding CSS classes to apply custom styling is also possible, though harder to do.

At present, the react-sdk offers no stable interface for components - this means properties and state can and do change at any time without notice. Once we determine the react-sdk to be stable enough to use as a proper SDK, we will adjust this policy. In the meantime, skinning is done completely at your own risk.

The approach you take is up to you - we suggest using a module replacement plugin, as found in webpack, though you're free to use whichever build system works for you. The react-sdk does not have any particular functions to call to load skins, so simply replace or extend the components/stores/etc you're after and build. As a reminder though, this is done completely at your own risk as we cannot guarantee a stable interface at this time.

Taking a look at element-web's approach to skinning may be worthwhile, as it overrides some relatively simple components.

The CIDER (Contenteditable-Input-Diff-Error-Reconcile) editor

The CIDER editor is a custom editor written for Element. Most of the code can be found in the /editor/ directory. It is used to power the composer main composer (both to send and edit messages), and might be used for other usecases where autocomplete is desired (invite box, ...).

High-level overview.

The editor is backed by a model that contains parts. A part has some text and a type (plain text, pill, ...). When typing in the editor, the model validates the input and updates the parts. The parts are then reconciled with the DOM.

Inner workings

When typing in the contenteditable element, the input event fires and the DOM of the editor is turned into a string. The way this is done has some logic to it to deal with adding newlines for block elements, to make sure the caret offset is calculated in the same way as the content string, and to ignore caret nodes (more on that later). For these reasons it doesn't use innerText, textContent or anything similar. The model addresses any content in the editor within as an offset within this string. The caret position is thus also converted from a position in the DOM tree to an offset in the content string. This happens in getCaretOffsetAndText in dom.ts.

Once the content string and caret offset is calculated, it is passed to the update() method of the model. The model first calculates the same content string of its current parts, basically just concatenating their text. It then looks for differences between the current and the new content string. The diffing algorithm is very basic, and assumes there is only one change around the caret offset, so this should be very inexpensive. See diff.ts for details.

The result of the diffing is the strings that were added and/or removed from the current content. These differences are then applied to the parts, where parts can apply validation logic to these changes.

For example, if you type an @ in some plain text, the plain text part rejects that character, and this character is then presented to the part creator, which will turn it into a pill candidate part. Pill candidate parts are what opens the auto completion, and upon picking a completion, replace themselves with an actual pill which can't be edited anymore.

The diffing is needed to preserve state in the parts apart from their text (which is the only thing the model receives from the DOM), e.g. to build the model incrementally. Any text that didn't change is assumed to leave the parts it intersects alone.

The benefit of this is that we can use the input event, which is broadly supported, to find changes in the editor. We don't have to rely on keyboard events, which relate poorly to text input or changes, and don't need the beforeinput event, which isn't broadly supported yet.

Once the parts of the model are updated, the DOM of the editor is then reconciled with the new model state, see renderModel in render.ts for this. If the model didn't reject the input and didn't make any additional changes, this won't make any changes to the DOM at all, and should thus be fairly efficient.

For the browser to allow the user to place the caret between two pills, or between a pill and the start and end of the line, we need some extra DOM nodes. These DOM nodes are called caret nodes, and contain an invisble character, so the caret can be placed into them. The model is unaware of caret nodes, and they are only added to the DOM during the render phase. Likewise, when calculating the content string, caret nodes need to be ignored, as they would confuse the model.

As part of the reconciliation, the caret position is also adjusted to any changes the model made to the input. The caret is passed around in two formats. The model receives the caret offset within the content string (which includes an atNodeEnd flag to make it unambiguous if it is at a part and or the next part start). The model converts this to a caret position internally, which has a partIndex and an offset within the part text, which is more natural to work with. From there on, the caret position is used, also during reconciliation.

Icons

Icons are loaded using @svgr/webpack. This is configured in element-web.

Each .svg exports a ReactComponent at the named export Icon. Icons have role="presentation" and aria-hidden automatically applied. These can be overriden by passing props to the icon component.

SVG file recommendations:

  • Colours should not be defined absolutely. Use currentColor instead.
  • SVG files should be taken from the design compound as they are. Some icons contain special padding. This means that there should be icons for each size, e.g. warning-16px and warning-32px.

Example usage:

import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';

const MyComponent = () => {
    return <>
        <FavoriteIcon className="mx_Icon mx_Icon_16">
    </>;
}

If possible, use the icon classes from here.

Custom styling

Icon components are svg elements and may be custom styled as usual.

_MyComponents.pcss:

.mx_MyComponent-icon {
    height: 20px;
    width: 20px;

    * {
        fill: $accent;
    }
}

MyComponent.tsx:

import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';

const MyComponent = () => {
    return <>
        <FavoriteIcon className="mx_MyComponent-icon" role="img" aria-hidden="false">
    </>;
}

Jitsi in Element

Element uses Jitsi for conference calls, which provides options for self-hosting your own server and supports most major platforms.

1:1 calls, or calls between you and one other person, do not use Jitsi. Instead, those calls work directly between clients or via TURN servers configured on the respective homeservers.

There's a number of ways to start a Jitsi call: the easiest way is to click on the voice or video buttons near the message composer in a room with more than 2 people. This will add a Jitsi widget which allows anyone in the room to join.

Integration managers (available through the 4 squares in the top right of the room) may provide their own approaches for adding Jitsi widgets.

Configuring Element to use your self-hosted Jitsi server

You can host your own Jitsi server to use with Element. It's usually advisable to use a recent version of Jitsi. In particular, versions older than around 6826 will cause problems with Element 1.9.10 or newer.

Element will use the Jitsi server that is embedded in the widget, even if it is not the one you configured. This is because conference calls must be held on a single Jitsi server and cannot be split over multiple servers.

However, you can configure Element to start a conference with your Jitsi server by adding to your config the following:

{
    "jitsi": {
        "preferred_domain": "your.jitsi.example.org"
    }
}

Element's default is meet.element.io (a free service offered by Element). meet.jit.si is an instance hosted by Jitsi themselves and is also free to use.

Once you've applied the config change, refresh Element and press the call button. This should start a new conference on your Jitsi server.

Note: The widget URL will point to a jitsi.html page hosted by Element. The Jitsi 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 default, and you are using element-web 1.6 or newer, set the following on your homeserver's /.well-known/matrix/client config:

{
    "im.vector.riot.jitsi": {
        "preferredDomain": "your.jitsi.example.org"
    }
}

Element Android

Element Android (1.0.5+) supports custom Jitsi domains, similar to Element Web above.

1:1 calls, or calls between you and one other person, do not use Jitsi. Instead, those calls work directly between clients or via TURN servers configured on the respective homeservers.

For rooms with more than 2 joined members, when creating a Jitsi conference via call/video buttons of the toolbar (not via integration manager), Element Android will create a widget using the wrapper hosted on app.element.io. The domain used is the one specified by the /.well-known/matrix/client endpoint, and if not present it uses the fallback defined in config.json (meet.element.io)

For active Jitsi widgets in the room, a native Jitsi widget UI is created and points to the instance specified in the domain key of the widget content data.

Element Android manages allowed native widgets permissions a bit differently than web widgets (as the data shared are different and never shared with the widget URL). For Jitsi widgets, permissions are requested only once per domain (consent saved in account data).

Jitsi Wrapper

Note: These are developer docs. Please consult your client's documentation for instructions on setting up Jitsi.

The react-sdk wraps all Jitsi call widgets in a local wrapper called jitsi.html which takes several parameters:

Query string:

  • widgetId: The ID of the widget. This is needed for communication back to the react-sdk.
  • parentUrl: The URL of the parent window. This is also needed for communication back to the react-sdk.

Hash/fragment (formatted as a query string):

  • conferenceDomain: The domain to connect Jitsi Meet to.
  • conferenceId: The room or conference ID to connect Jitsi Meet to.
  • isAudioOnly: Boolean for whether this is a voice-only conference. May not be present, should default to false.
  • startWithAudioMuted: Boolean for whether the calls start with audio muted. May not be present.
  • startWithVideoMuted: Boolean for whether the calls start with video muted. May not be present.
  • displayName: The display name of the user viewing the widget. May not be present or could be null.
  • avatarUrl: The HTTP(S) URL for the avatar of the user viewing the widget. May not be present or could be null.
  • userId: The MXID of the user viewing the widget. May not be present or could be null.

The react-sdk will assume that jitsi.html is at the path of wherever it is currently being served. For example, https://develop.element.io/jitsi.html or vector://webapp/jitsi.html.

The jitsi.html wrapper can use the react-sdk's WidgetApi to communicate, making it easier to actually implement the feature.

Local echo (developer docs)

The React SDK provides some local echo functionality to allow for components to do something quickly and fall back when it fails. This is all available in the local-echo directory within stores.

Echo is handled in EchoChambers, with GenericEchoChamber being the base implementation for all chambers. The EchoChamber class is provided as semantic access to a GenericEchoChamber implementation, such as the RoomEchoChamber (which handles echoable details of a room).

Anything that can be locally echoed will be provided by the GenericEchoChamber implementation. The echo chamber will also need to deal with external changes, and has full control over whether or not something has successfully been echoed.

An EchoContext is provided to echo chambers (usually with a matching type: RoomEchoContext gets provided to a RoomEchoChamber for example) with details about their intended area of effect, as well as manage EchoTransactions. An EchoTransaction is simply a unit of work that needs to be locally echoed.

The EchoStore manages echo chamber instances, builds contexts, and is generally less semantically accessible than the EchoChamber class. For separation of concerns, and to try and keep things tidy, this is an intentional design decision.

Note: The local echo stack uses a "whenable" pattern, which is similar to thenables and EventEmitter. Whenables are ways of actioning a changing condition without having to deal with listeners being torn down. Once the reference count of the Whenable causes garbage collection, the Whenable's listeners will also be torn down. This is accelerated by the IDestroyable interface usage.

Audit functionality

The UI supports a "Server isn't responding" dialog which includes a partial audit log-like structure to it. This is partially the reason for added complexity of EchoTransactions and EchoContexts - this information feeds the UI states which then provide direct retry mechanisms.

The EchoStore is responsible for ensuring that the appropriate non-urgent toast (lower left) is set up, where the dialog then drives through the contexts and transactions.

Media handling

Surely media should be as easy as just putting a URL into an img and calling it good, right? Not quite. Matrix uses something called a Matrix Content URI (better known as MXC URI) to identify content, which is then converted to a regular HTTPS URL on the homeserver. However, sometimes that URL can change depending on deployment considerations.

The react-sdk features a customisation endpoint for media handling where all conversions from MXC URI to HTTPS URL happen. This is to ensure that those obscure deployments can route all their media to the right place.

For development, there are currently two functions available: mediaFromMxc and mediaFromContent. The mediaFromMxc function should be self-explanatory. mediaFromContent takes an event content as a parameter and will automatically parse out the source media and thumbnail. Both functions return a Media object with a number of options on it, such as getting various common HTTPS URLs for the media.

It is extremely important that all media calls are put through this customisation endpoint. So much so it's a lint rule to avoid accidental use of the wrong functions.

Room list sorting

It's so complicated it needs its own README.

Legend:

  • Orange = External event.
  • Purple = Deterministic flow.
  • Green = Algorithm definition.
  • Red = Exit condition/point.
  • Blue = Process definition.

Algorithms involved

There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting. Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting Algorithm respectively. The list algorithm determines the primary ordering of a given tag whereas the tag sorting defines how rooms within that tag get sorted, at the discretion of the list ordering.

Behaviour of the overall room list (sticky rooms, etc) are determined by the generically-named Algorithm class. Here is where much of the coordination from the room list store is done to figure out which list algorithm to call, instead of having all the logic in the room list store itself.

Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm, later described in this document, heavily uses the list ordering behaviour to break the tag into categories. Each category then gets sorted by the appropriate tag sorting algorithm.

Tag sorting algorithm: Alphabetical

When used, rooms in a given tag will be sorted alphabetically, where the alphabet's order is a problem for the browser. All we do is a simple string comparison and expect the browser to return something useful.

Tag sorting algorithm: Manual

Manual sorting makes use of the order property present on all tags for a room, per the Matrix specification. Smaller values of order cause rooms to appear closer to the top of the list.

Tag sorting algorithm: Recent

Rooms get ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm in the room list system which determines whether an event type is capable of bubbling up in the room list. Normally events like room messages, stickers, and room security changes will be considered useful enough to cause a shift in time.

Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually consistent this means that from time to time a room might plummet or skyrocket across the tag due to the timestamp contained within the event (generated server-side by the sender's server).

List ordering algorithm: Natural

This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list. Historically, it's been the only option in Element and extremely common in most chat applications due to its relative deterministic behaviour.

List ordering algorithm: Importance

On the other end of the spectrum, this is the most complicated algorithm which exists. There's major behavioural changes, and the tag sorting algorithm gets selectively applied depending on circumstances.

Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags simply get the manual sorting algorithm applied to them with no further involvement from the importance algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off relative (perceived) importance to the user:

  • Red: The room has unread mentions waiting for the user.
  • Grey: The room has unread notifications waiting for the user. Notifications are simply unread messages which cause a push notification or badge count. Typically, this is the default as rooms get set to 'All Messages'.
  • Bold: The room has unread messages waiting for the user. Essentially this is a grey room without a badge/notification count (or 'Mentions Only'/'Muted').
  • Idle: No useful (see definition of useful above) activity has occurred in the room since the user last read it.

Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey above bold, etc.

Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm gets applied to each category in a sub-list fashion. This should result in the red rooms (for example) being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but collectively the tag will be sorted into categories with red being at the top.

Sticky rooms

When the user visits a room, that room becomes 'sticky' in the list, regardless of ordering algorithm. From a code perspective, the underlying algorithm is not aware of a sticky room and instead the base class manages which room is sticky. This is to ensure that all algorithms handle it the same.

The sticky flag is simply to say it will not move higher or lower down the list while it is active. For example, if using the importance algorithm, the room would naturally become idle once viewed and thus would normally fly down the list out of sight. The sticky room concept instead holds it in place, never letting it fly down until the user moves to another room.

Only one room can be sticky at a time. Room updates around the sticky room will still hold the sticky room in place. The best example of this is the importance algorithm: if the user has 3 red rooms and selects the middle room, they will see exactly one room above their selection at all times. If they receive another notification which causes the room to move into the topmost position, the room that was above the sticky room will move underneath to allow for the new room to take the top slot, maintaining the sticky room's position.

Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries and thus the user can see a shift in what kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having the rooms above it read on another device. This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain 2 rooms above the sticky room.

An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed. The N value will never increase while selection remains unchanged: adding a bunch of rooms after having put the sticky room in a position where it's had to decrease N will not increase N.

Responsibilities of the store

The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets an object containing the tags it needs to worry about and the rooms within. The room list component will decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with all kinds of filtering.

Filtering

Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime.

Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of rooms to the user. The algorithm implementations will not see a room being prefiltered out.

Runtime filters are used for more dynamic filtering, such as the user filtering by room name. These filters are passed along to the algorithm implementations where those implementations decide how and when to apply the filter. In practice, the base Algorithm class ends up doing the heavy lifting for optimization reasons.

The results of runtime filters get cached to avoid needlessly iterating over potentially thousands of rooms, as the old room list store does. When a filter condition changes, it emits an update which (in this case) the Algorithm class will pick up and act accordingly. Typically, this also means filtering a minor subset where possible to avoid over-iterating rooms.

All filter conditions are considered "stable" by the consumers, meaning that the consumer does not expect a change in the condition unless the condition says it has changed. This is intentional to maintain the caching behaviour described above.

One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight subtlety: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where room notifications are self-contained within that workspace. Runtime filters tend to not want to affect visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead, the notification counts would vary while the user was typing and "found 2/12" UX would not be possible.

Class breakdowns

The RoomListStore is the major coordinator of various algorithm implementations, which take care of the various ListAlgorithm and SortingAlgorithm options. The Algorithm class is responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the user). Various list-specific utilities are also included, though they are expected to move somewhere more general when needed. For example, the membership utilities could easily be moved elsewhere as needed.

The various bits throughout the room list store should also have jsdoc of some kind to help describe what they do and how they work.

ScrollPanel

Updates

During an onscroll event, we check whether we're getting close to the top or bottom edge of the loaded content. If close enough, we fire a request to load more through the callback passed in the onFillRequest prop. This returns a promise is passed down from TimelinePanel, where it will call paginate on the TimelineWindow and once the events are received back, update its state with the new events. This update trickles down to the MessagePanel, which rerenders all tiles and passed that to ScrollPanel. ScrollPanels componentDidUpdate method gets called, and we do the scroll housekeeping there (read below). Once the rerender has completed, the setState callback is called and we resolve the promise returned by onFillRequest. Now we check the DOM to see if we need more fill requests.

Prevent Shrinking

ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a jump when at the bottom of the timeline and people start and stop typing. It gets cleared automatically when 200px above the bottom of the timeline.

BACAT (Bottom-Aligned, Clipped-At-Top) scrolling

BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842.

The motivation for the changes is having noticed that setting scrollTop while scrolling tends to not work well, with it interrupting ongoing scrolling and also querying scrollTop reporting outdated values and consecutive scroll adjustments cancelling each out previous ones. This seems to be worse on macOS than other platforms, presumably because of a higher resolution in scroll events there. Also see https://github.com/vector-im/element-web/issues/528. The BACAT approach allows to only have to change the scroll offset when adding or removing tiles.

The approach taken instead is to vertically align the timeline tiles to the bottom of the scroll container (using flexbox) and give the timeline inside the scroll container an explicit height, initially set to a multiple of the PAGE_SIZE (400px at time of writing) as needed by the content. When scrolled up, we can compensate for anything that grew below the viewport by changing the height of the timeline to maintain what's currently visible in the viewport without adjusting the scrollTop and hence without jumping.

For anything above the viewport growing or shrinking, we don't need to do anything as the timeline is bottom-aligned. We do need to update the height manually to keep all content visible as more is loaded. To maintain scroll position after the portion above the viewport changes height, we need to set the scrollTop, as we cannot balance it out with more height changes. We do this 100ms after the user has stopped scrolling, so setting scrollTop has not nasty side-effects.

As of https://github.com/matrix-org/matrix-react-sdk/pull/4166, we are scrolling to compensate for height changes by calling scrollBy(0, x) rather than reading and than setting scrollTop, as reading scrollTop can (again, especially on macOS) easily return values that are out of sync with what is on the screen, probably because scrolling can be done off the main thread in some circumstances. This seems to further prevent jumps.

How does it work?

componentDidUpdate is called when a tile in the timeline is updated (as we rerender the whole timeline) or tiles are added or removed (see Updates section before). From here, checkScroll is called, which calls restoreSavedScrollState. Now, we increase the timeline height if something below the viewport grew by adjusting this.bottomGrowth. bottomGrowth is the height added to the timeline (on top of the height from the number of pages calculated at the last updateHeight run) to compensate for growth below the viewport. This is cleared during the next run of updateHeight. Remember that the tiles in the timeline are aligned to the bottom.

From restoreSavedScrollState we also call updateHeight which waits until the user stops scrolling for 100ms and then recalculates the amount of pages of 400px the timeline should be sized to, to be able to show all of its (newly added) content. We have to adjust the scroll offset (which is why we wait until scrolling has stopped) now because the space above the viewport has likely changed.

Usercontent

While decryption itself is safe to be done without a sandbox, letting the browser and user interact with the resulting data may be dangerous, previously usercontent.riot.im was used to act as a sandbox on a different origin to close the attack surface, it is now possible to do by using a combination of a sandboxed iframe and some code written into the app which consumes this SDK.

Usercontent is an iframe sandbox target for allowing a user to safely download a decrypted attachment from a sandboxed origin where it cannot be used to XSS your Element session out from under you.

Its function is to create an Object URL for the user/browser to use but bound to an origin different to that of the Element instance to protect against XSS.

It exposes a function over a postMessage API, when sent an object with the matching fields to render a download link with the Object URL:

{
    imgSrc: "", // the src of the image to display in the download link
    imgStyle: "", // the style to apply to the image
    style: "", // the style to apply to the download link
    download: "", // download attribute to pass to the <a/> tag
    textContent: "", // the text to put inside the download link
    blob: "", // the data blob to wrap in an object url and allow the user to download
}

If only imgSrc, imgStyle and style are passed then just update the existing link without overwriting other things about it.

It is expected that this target be available at usercontent/ relative to the root of the app, this can be seen in element-web's webpack config.

Widget layout support

Rooms can have a default widget layout to auto-pin certain widgets, make the container different sizes, etc. These are defined through the io.element.widgets.layout state event (empty state key).

Full example content:

{
    widgets: {
        "first-widget-id": {
            container: "top",
            index: 0,
            width: 60,
            height: 40,
        },
        "second-widget-id": {
            container: "right",
        },
    },
}

As shown, there are two containers possible for widgets. These containers have different behaviour and interpret the other options differently.

top container

This is the "App Drawer" or any pinned widgets in a room. This is by far the most versatile container though does introduce potential usability issues upon members of the room (widgets take up space and therefore fewer messages can be shown).

The index for a widget determines which order the widgets show up in from left to right. Widgets without an index will show up as the rightmost widgets. Tiebreaks (same index or multiple defined without an index) are resolved by comparing widget IDs. A maximum of 3 widgets can be in the top container - any which exceed this will be ignored (placed into the right container). Smaller numbers represent leftmost widgets.

The width is relative width within the container in percentage points. This will be clamped to a range of 0-100 (inclusive). The widgets will attempt to scale to relative proportions when more than 100% space is allocated. For example, if 3 widgets are defined at 40% width each then the client will attempt to show them at 33% width each.

Note that the client may impose minimum widths on the widgets, such as a 10% minimum to avoid pinning hidden widgets. In general, widgets defined in the 30-70% range each will be free of these restrictions.

The height is not in fact applied per-widget but is recorded per-widget for potential future capabilities in future containers. The top container will take the tallest height and use that for the height of the whole container, and thus all widgets in that container. The height is relative to the container, like with width, meaning that 100% will consume as much space as the client is willing to sacrifice to the widget container. Like with width, the client may impose minimums to avoid the container being uselessly small. Heights in the 30-100% range are generally acceptable. The height is also clamped to be within 0-100, inclusive.

right container

This is the default container and has no special configuration. Widgets which overflow from the top container will be put in this container instead. Putting a widget in the right container does not automatically show it - it only mentions that widgets should not be in another container.

The behaviour of this container may change in the future.

yarn run v1.22.22 $ /home/runner/work/element-web/element-web/element-web/node_modules/.bin/ts-node ./scripts/gen-workflow-mermaid.ts ../element-desktop ../element-web ../matrix-js-sdk

---
title: At 09:00 AM & element-desktop Release & Manual
---
flowchart LR
    subgraph ID0["Build and Deploy"]
        ID1-- needs -->ID2
        ID1-- needs -->ID3
        ID1-- needs -->ID4
        ID1-- needs -->ID5
        ID3-- needs -->ID5
        ID4-- needs -->ID5
        ID2-- needs -->ID5
        ID5-- needs -->ID6
        ID1[["prepare"]]
        click ID1 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_deploy.yaml" "Click to open workflow"
        subgraph ID2["Windows "]
            ID7[["Windows ia32"]]
            click ID7 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_deploy.yaml" "Click to open workflow"
            ID8[["Windows x64"]]
            click ID8 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_deploy.yaml" "Click to open workflow"
        end
        ID3[["macOS"]]
        click ID3 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_deploy.yaml" "Click to open workflow"
        subgraph ID4["Linux )"]
            ID9[["Linux amd64 (sqlcipher static)"]]
            click ID9 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_deploy.yaml" "Click to open workflow"
            IDa[["Linux arm64 (sqlcipher static)"]]
            click IDa href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_deploy.yaml" "Click to open workflow"
        end
        ID5[["${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }}"]]
        click ID5 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_deploy.yaml" "Click to open workflow"
        ID6[["Deploy builds to ESS"]]
        click ID6 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_deploy.yaml" "Click to open workflow"
    end
    IDb(("At 09:00 AM"))
    IDc(("element-desktop Release"))
    IDd(("Manual"))
    IDb --> ID0
    IDc --> ID0
    IDd --> ID0
---
title: Pull Request element-desktop & Push element-desktop develop & Push element-desktop master & Push element-desktop staging & Manual
---
flowchart LR
    subgraph ID0["Build and Test"]
        ID1-- needs -->ID2
        ID1-- needs -->ID3
        ID1-- needs -->ID4
        ID4-- needs -->ID5
        ID3-- needs -->ID5
        ID2-- needs -->ID5
        ID1[["fetch"]]
        click ID1 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
        subgraph ID2["Windows"]
            ID6[["Windows (x64)"]]
            click ID6 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            ID7[["Windows (ia32)"]]
            click ID7 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
        end
        subgraph ID3["Linux "]
            ID8[["Linux (amd64) (sqlcipher: system)"]]
            click ID8 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            ID9[["Linux (arm64) (sqlcipher: system)"]]
            click ID9 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            IDa[["Linux (amd64) (sqlcipher: static)"]]
            click IDa href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            IDb[["Linux (arm64) (sqlcipher: static)"]]
            click IDb href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
        end
        ID4[["macOS"]]
        click ID4 href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
        subgraph ID5["Test "]
            IDc[["Test macOS Universal"]]
            click IDc href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            IDd[["Test Linux (amd64) (sqlcipher: system)"]]
            click IDd href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            IDe[["Test Linux (amd64) (sqlcipher: static)"]]
            click IDe href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            IDf[["Test Linux (arm64) (sqlcipher: system)"]]
            click IDf href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            IDg[["Test Linux (arm64) (sqlcipher: static)"]]
            click IDg href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            IDh[["Test Windows (x86)"]]
            click IDh href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
            IDi[["Test Windows (x64)"]]
            click IDi href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_and_test.yaml" "Click to open workflow"
        end
    end
    IDj(("Pull Request<br>element-desktop"))
    IDk(("Push element-desktop<br>develop"))
    subgraph IDl["Dockerbuild"]
        IDm[["Docker Build"]]
        click IDm href "https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/dockerbuild.yaml" "Click to open workflow"
    end
    IDn(("Push element-desktop<br>master"))
    IDo(("Push element-desktop<br>staging"))
    IDp(("Manual"))
    IDj --> ID0
    IDk --> ID0
    IDo --> ID0
    IDn --> ID0
    IDk --> IDl
    IDn --> IDl
    IDo --> IDl
    IDp --> IDl
---
title: At 06:00 AM only on Monday Wednesday and Friday & Manual
---
flowchart LR
    subgraph ID0["Localazy Download"]
        ID1[["download"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/localazy_download.yaml" "Click to open workflow"
    end
    ID2(("At 06:00 AM<br>only on Monday<br>Wednesday<br>and Friday"))
    ID3(("Manual"))
    ID2 --> ID0
    ID3 --> ID0
---
title: Push element-web develop & Pull Request element-web & Push element-web master & Manual
---
flowchart LR
    subgraph ID0["Localazy Upload"]
        ID1[["upload"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/localazy_upload.yaml" "Click to open workflow"
    end
    ID2(("Push element-web<br>develop"))
    subgraph ID3["Build"]
        subgraph ID4["Build on "]
            ID5[["Build on ubuntu-24.04"]]
            click ID5 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/build.yml" "Click to open workflow"
            ID6[["Build on windows-2022"]]
            click ID6 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/build.yml" "Click to open workflow"
            ID7[["Build on macos-14"]]
            click ID7 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/build.yml" "Click to open workflow"
        end
    end
    ID8(("Pull Request<br>element-web"))
    subgraph ID9["End to End Tests"]
        IDa-- needs -->IDb
        IDb-- needs -->IDc
        IDa[["Build Element-Web"]]
        click IDa href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests.yaml" "Click to open workflow"
        subgraph IDb["Run Tests "]
            IDd[["Run Tests 1/${{ strategy.job-total }}"]]
            click IDd href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests.yaml" "Click to open workflow"
            IDe[["Run Tests 2/${{ strategy.job-total }}"]]
            click IDe href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests.yaml" "Click to open workflow"
            IDf[["Run Tests 3/${{ strategy.job-total }}"]]
            click IDf href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests.yaml" "Click to open workflow"
            IDg[["Run Tests 4/${{ strategy.job-total }}"]]
            click IDg href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests.yaml" "Click to open workflow"
            IDh[["Run Tests 5/${{ strategy.job-total }}"]]
            click IDh href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests.yaml" "Click to open workflow"
            IDi[["Run Tests 6/${{ strategy.job-total }}"]]
            click IDi href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests.yaml" "Click to open workflow"
        end
        IDc[["end-to-end-tests"]]
        click IDc href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests.yaml" "Click to open workflow"
    end
    subgraph IDj["Upload End to End Test report to Netlify"]
        IDk[["Report results"]]
        click IDk href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/end-to-end-tests-netlify.yaml" "Click to open workflow"
    end
    IDl(("Push element-web<br>master"))
    subgraph IDm["Upload Preview Build to Netlify"]
        IDn[["deploy"]]
        click IDn href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/netlify.yaml" "Click to open workflow"
    end
    subgraph IDo["Pull Request Base Branch"]
        IDp[["Check PR base branch"]]
        click IDp href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/pull_request_base_branch.yaml" "Click to open workflow"
    end
    subgraph IDq["Build and Deploy develop"]
        IDr[["Build & Deploy develop.element.io"]]
        click IDr href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/build_develop.yml" "Click to open workflow"
    end
    subgraph IDs["Deploy documentation"]
        IDt-- needs -->IDu
        IDt[["GitHub Pages"]]
        click IDt href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/docs.yml" "Click to open workflow"
        IDu[["deploy"]]
        click IDu href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/docs.yml" "Click to open workflow"
    end
    IDv(("Manual"))
    ID2 --> ID0
    ID2 --> ID3
    ID2 --> IDq
    ID2 --> IDs
    ID2 --> ID9
    ID8 --> ID3
    IDl --> ID3
    ID8 --> ID9
    ID8 --> IDo
    ID9-- workflow_run -->IDj
    IDl --> ID9
    ID9-- workflow_run -->IDm
    IDv --> IDs
---
title: Push matrix-js-sdk staging & Manual
---
flowchart LR
    subgraph ID0["Release Drafter"]
        ID1[["draft"]]
        click ID1 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/release-drafter.yml" "Click to open workflow"
    end
    ID2(("Push matrix-js-sdk<br>staging"))
    ID3(("Manual"))
    ID2 --> ID0
    ID3 --> ID0
---
title: Push matrix-js-sdk master & Pull Request matrix-js-sdk & Push matrix-js-sdk develop & At 01:00 AM & Manual
---
flowchart LR
    subgraph ID0["Merge master -> develop"]
        ID1[["merge"]]
        click ID1 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/release-gitflow.yml" "Click to open workflow"
    end
    ID2(("Push matrix-js-sdk<br>master"))
    subgraph ID3["Static Analysis"]
        ID4[["Typescript Syntax Check"]]
        click ID4 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow"
        ID5[["ESLint"]]
        click ID5 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow"
        ID6[["Workflow Lint"]]
        click ID6 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow"
        ID7[["JSDoc Checker"]]
        click ID7 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow"
        ID8[["Analyse Dead Code"]]
        click ID8 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow"
    end
    ID9(("Pull Request<br>matrix-js-sdk"))
    subgraph IDa["Tests"]
        IDb-- needs -->IDc
        IDd-- needs -->IDe
        IDf-- needs -->IDg
        subgraph IDb["Jest [)"]
            IDh[["Jest [integ] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"]]
            click IDh href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
            IDi[["Jest [integ] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"]]
            click IDi href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
            IDj[["Jest [unit] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"]]
            click IDj href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
            IDk[["Jest [unit] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"]]
            click IDk href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
        end
        IDc[["Jest tests"]]
        click IDc href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
        IDf[["Downstream test element-web"]]
        click IDf href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
        IDd[["Run Complement Crypto tests"]]
        click IDd href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
        IDe[["Downstream Complement Crypto tests"]]
        click IDe href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
        IDg[["Downstream tests"]]
        click IDg href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow"
    end
    subgraph IDl["SonarQube"]
        IDm[["🩻 SonarQube"]]
        click IDm href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/sonarqube.yml" "Click to open workflow"
    end
    IDn(("Push matrix-js-sdk<br>develop"))
    subgraph IDo["Sync labels"]
        IDp[["sync-labels"]]
        click IDp href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/sync-labels.yml" "Click to open workflow"
    end
    IDq(("At 01:00 AM"))
    subgraph IDr["Notify Downstream Projects"]
        subgraph IDs["notify-downstream"]
            IDt[["notify-downstream (element-hq/element-web, element-web-notify)"]]
            click IDt href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/notify-downstream.yaml" "Click to open workflow"
        end
    end
    subgraph IDu["matrix-react-sdk End to End Tests"]
        IDv[["Playwright"]]
        click IDv href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/downstream-end-to-end-tests.yml" "Click to open workflow"
    end
    subgraph IDw["Deploy documentation PR preview"]
        IDx[["netlify"]]
        click IDx href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/docs-pr-netlify.yaml" "Click to open workflow"
    end
    IDy(("Manual"))
    ID2 --> ID0
    ID2 --> ID3
    ID2 --> IDa
    ID9 --> ID3
    IDn --> ID3
    ID3-- workflow_run -->IDw
    ID9 --> IDa
    ID9 --> IDu
    IDa-- workflow_run -->IDl
    IDn --> IDa
    IDn --> IDo
    IDn --> IDr
    IDq --> IDo
    IDy --> IDo
---
title: Manual
---
flowchart LR
    subgraph ID0["Release Process"]
        ID1-- needs -->ID2
        ID2-- needs -->ID3
        ID1[["release"]]
        click ID1 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/release.yml" "Click to open workflow"
        ID2[["Publish Documentation"]]
        click ID2 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/release.yml" "Click to open workflow"
        ID3[["docs-deploy"]]
        click ID3 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/release.yml" "Click to open workflow"
    end
    ID4(("Manual"))
    ID4 --> ID0
---
title: matrix-js-sdk Issues
---
flowchart LR
    subgraph ID0["Move new issues into Issue triage board"]
        ID1[["automate-project-columns-next"]]
        click ID1 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/triage-incoming.yml" "Click to open workflow"
    end
    ID2(("matrix-js-sdk Issues"))
    subgraph ID3["Move labelled issues to correct projects"]
        ID4[["call-triage-labelled"]]
        click ID4 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/triage-labelled.yml" "Click to open workflow"
    end
    ID2 --> ID0
    ID2 --> ID3
---
title: element-web Release
---
flowchart LR
    subgraph ID0["Build Debian package"]
        ID1[["Build package"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/build_debian.yaml" "Click to open workflow"
    end
    ID2(("element-web Release"))
    ID2 --> ID0
---
title: Push element-web tag v* & At 0 minutes past the hour every 12 hours starting at 07:00 AM & Manual
---
flowchart LR
    subgraph ID0["Dockerhub"]
        ID1[["Docker Buildx"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/dockerhub.yaml" "Click to open workflow"
    end
    ID2(("Push element-web<br>tag v*"))
    ID3(("At 0 minutes past the hour<br>every 12 hours<br>starting at 07:00 AM"))
    ID4(("Manual"))
    ID2 --> ID0
    ID3 --> ID0
    ID4 --> ID0
---
title: element-web Issues
---
flowchart LR
    subgraph ID0["issue_closed.yml"]
        ID1[["Tidy closed issues"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/issue_closed.yml" "Click to open workflow"
    end
    ID2(("element-web Issues"))
    subgraph ID3["Move issued assigned to specific team members to their boards"]
        ID4[["web-app-team"]]
        click ID4 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/triage-assigned.yml" "Click to open workflow"
    end
    subgraph ID5["Move unlabelled from needs info columns to triaged"]
        ID6[["Move no longer X-Needs-Info issues to Triaged"]]
        click ID6 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/triage-unlabelled.yml" "Click to open workflow"
        ID7[["Remove Z-Labs label when features behind labs flags are removed"]]
        click ID7 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/triage-unlabelled.yml" "Click to open workflow"
    end
    ID2 --> ID0
    ID2 --> ID3
    ID2 --> ID5
---
title: Manual
---
flowchart LR
    subgraph ID0["Pending reviews automation"]
        ID1[["Pending reviews bot"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/pending-reviews.yaml" "Click to open workflow"
    end
    ID2(("Manual"))
    ID2 --> ID0
---
title: At 06:00 AM & Manual
---
flowchart LR
    subgraph ID0["Update Playwright docker images"]
        ID1[["update"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/playwright-image-updates.yaml" "Click to open workflow"
    end
    ID2(("At 06:00 AM"))
    ID3(("Manual"))
    ID2 --> ID0
    ID3 --> ID0
---
title: Manual
---
flowchart LR
    subgraph ID0["Cut branches"]
        ID1[["prepare"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/release_prepare.yml" "Click to open workflow"
    end
    ID2(("Manual"))
    ID2 --> ID0
---
title: At 01:30 AM
---
flowchart LR
    subgraph ID0["Close stale flaky issues"]
        ID1[["close"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/triage-stale-flaky-tests.yml" "Click to open workflow"
    end
    ID2(("At 01:30 AM"))
    ID2 --> ID0
---
title: At 03:00 AM only on Sunday & Manual
---
flowchart LR
    subgraph ID0["Update Jitsi"]
        ID1[["update"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/update-jitsi.yml" "Click to open workflow"
    end
    ID2(("At 03:00 AM<br>only on Sunday"))
    ID3(("Manual"))
    ID2 --> ID0
    ID3 --> ID0
---
title: Manual
---
flowchart LR
    subgraph ID0["Update release topics"]
        ID1[["Release topic update"]]
        click ID1 href "https://github.com/element-hq/element-web/blob/develop/.github/workflows/update-topics.yaml" "Click to open workflow"
    end
    ID2(("Manual"))
    ID2 --> ID0

Done in 6.27s.

npm Tests Static Analysis Quality Gate Status Coverage Vulnerabilities Bugs

Matrix JavaScript SDK

This is the Matrix Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a browser or in Node.js.

Minimum Matrix server version: v1.1

The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports is removed in v1.4 then the feature is eligible for removal from the SDK when v1.8 is released. This SDK has no guarantee on implementing all features of any particular spec release, currently. This can mean that the SDK will call endpoints from before Matrix 1.1, for example.

Quickstart

[!IMPORTANT] Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the Authenticated Media section for information on how to enable support for this.

Using yarn instead of npm is recommended. Please see the Yarn install guide if you do not have it already.

yarn add matrix-js-sdk

import * as sdk from "matrix-js-sdk";
const client = sdk.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms(function (err, data) {
    console.log("Public Rooms: %s", JSON.stringify(data));
});

See below for how to include libolm to enable end-to-end-encryption. Please check the Node.js terminal app for a more complex example.

To start the client:

await client.startClient({ initialSyncLimit: 10 });

You can perform a call to /sync to get the current state of the client:

client.once(ClientEvent.sync, function (state, prevState, res) {
    if (state === "PREPARED") {
        console.log("prepared");
    } else {
        console.log(state);
        process.exit(1);
    }
});

To send a message:

const content = {
    body: "message text",
    msgtype: "m.text",
};
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
    console.log(err);
});

To listen for message events:

client.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
    if (event.getType() !== "m.room.message") {
        return; // only use messages
    }
    console.log(event.event.content.body);
});

By default, the matrix-js-sdk client uses the MemoryStore to store events as they are received. For example to iterate through the currently stored timeline for a room:

Object.keys(client.store.rooms).forEach((roomId) => {
    client.getRoom(roomId).timeline.forEach((t) => {
        console.log(t.event);
    });
});

Authenticated media

Servers supporting MSC3916 (Matrix 1.11) will require clients, like yours, to include an Authorization header when /downloading or /thumbnailing media. For NodeJS environments this may be as easy as the following code snippet, though web browsers may need to use Service Workers to append the header when using the endpoints in <img /> elements and similar.

const downloadUrl = client.mxcUrlToHttp(
    /*mxcUrl=*/ "mxc://example.org/abc123", // the MXC URI to download/thumbnail, typically from an event or profile
    /*width=*/ undefined, // part of the thumbnail API. Use as required.
    /*height=*/ undefined, // part of the thumbnail API. Use as required.
    /*resizeMethod=*/ undefined, // part of the thumbnail API. Use as required.
    /*allowDirectLinks=*/ false, // should generally be left `false`.
    /*allowRedirects=*/ true, // implied supported with authentication
    /*useAuthentication=*/ true, // the flag we're after in this example
);
const img = await fetch(downloadUrl, {
    headers: {
        Authorization: `Bearer ${client.getAccessToken()}`,
    },
});
// Do something with `img`.

[!WARNING] In future the js-sdk will only return authentication-required URLs, mandating population of the Authorization header.

What does this SDK do?

This SDK provides a full object model around the Matrix Client-Server API and emits events for incoming data and state changes. Aside from wrapping the HTTP API, it:

  • Handles syncing (via /sync)
  • Handles the generation of "friendly" room and member names.
  • Handles historical RoomMember information (e.g. display names).
  • Manages room member state across multiple events (e.g. it handles typing, power levels and membership changes).
  • Exposes high-level objects like Rooms, RoomState, RoomMembers and Users which can be listened to for things like name changes, new messages, membership changes, presence changes, and more.
  • Handle "local echo" of messages sent using the SDK. This means that messages that have just been sent will appear in the timeline as 'sending', until it completes. This is beneficial because it prevents there being a gap between hitting the send button and having the "remote echo" arrive.
  • Mark messages which failed to send as not sent.
  • Automatically retry requests to send messages due to network errors.
  • Automatically retry requests to send messages due to rate limiting errors.
  • Handle queueing of messages.
  • Handles pagination.
  • Handle assigning push actions for events.
  • Handles room initial sync on accepting invites.
  • Handles WebRTC calling.

Usage

Supported platforms

matrix-js-sdk can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed), or in browser applications, via a bundler such as Webpack or Vite.

You can also use the sdk with Deno (import npm:matrix-js-sdk) but its not officialy supported.

Emitted events

The SDK raises notifications to the application using EventEmitters. The MatrixClient itself implements EventEmitter, as do many of the high-level abstractions such as Room and RoomMember.

// Listen for low-level MatrixEvents
client.on(ClientEvent.Event, function (event) {
    console.log(event.getType());
});

// Listen for typing changes
client.on(RoomMemberEvent.Typing, function (event, member) {
    if (member.typing) {
        console.log(member.name + " is typing...");
    } else {
        console.log(member.name + " stopped typing.");
    }
});

// start the client to setup the connection to the server
client.startClient();

Entry points

As well as the primary entry point (matrix-js-sdk), there are several other entry points which may be useful:

Entry pointDescription
matrix-js-sdkPrimary entry point. High-level functionality, and lots of historical clutter in need of a cleanup.
matrix-js-sdk/lib/crypto-apiCryptography functionality.
matrix-js-sdk/lib/typesLow-level types, reflecting data structures defined in the Matrix spec.
matrix-js-sdk/lib/testingTest utilities, which may be useful in test code but should not be used in production code.
matrix-js-sdk/lib/utils/*.jsA set of modules exporting standalone functions (and their types).

Examples

This section provides some useful code snippets which demonstrate the core functionality of the SDK. These examples assume the SDK is set up like this:

import * as sdk from "matrix-js-sdk";
const myUserId = "@example:localhost";
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
const matrixClient = sdk.createClient({
    baseUrl: "http://localhost:8008",
    accessToken: myAccessToken,
    userId: myUserId,
});

Automatically join rooms when invited

matrixClient.on(RoomEvent.MyMembership, function (room, membership, prevMembership) {
    if (membership === KnownMembership.Invite) {
        matrixClient.joinRoom(room.roomId).then(function () {
            console.log("Auto-joined %s", room.roomId);
        });
    }
});

matrixClient.startClient();
matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
    if (toStartOfTimeline) {
        return; // don't print paginated results
    }
    if (event.getType() !== "m.room.message") {
        return; // only print messages
    }
    console.log(
        // the room name will update with m.room.name events automatically
        "(%s) %s :: %s",
        room.name,
        event.getSender(),
        event.getContent().body,
    );
});

matrixClient.startClient();

Output:

  (My Room) @megan:localhost :: Hello world
  (My Room) @megan:localhost :: how are you?
  (My Room) @example:localhost :: I am good
  (My Room) @example:localhost :: change the room name
  (My New Room) @megan:localhost :: done
matrixClient.on(RoomStateEvent.Members, function (event, state, member) {
    const room = matrixClient.getRoom(state.roomId);
    if (!room) {
        return;
    }
    const memberList = state.getMembers();
    console.log(room.name);
    console.log(Array(room.name.length + 1).join("=")); // underline
    for (var i = 0; i < memberList.length; i++) {
        console.log("(%s) %s", memberList[i].membership, memberList[i].name);
    }
});

matrixClient.startClient();

Output:

  My Room
  =======
  (join) @example:localhost
  (leave) @alice:localhost
  (join) Bob
  (invite) @charlie:localhost

API Reference

A hosted reference can be found at http://matrix-org.github.io/matrix-js-sdk/index.html

This SDK uses Typedoc doc comments. You can manually build and host the API reference from the source files like this:

  $ yarn gendoc
  $ cd docs
  $ python -m http.server 8005

Then visit http://localhost:8005 to see the API docs.

End-to-end encryption support

This section is outdated. Use of libolm is deprecated and we are replacing it with support from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972).

The SDK supports end-to-end encryption via the Olm and Megolm protocols, using libolm. It is left up to the application to make libolm available, via the Olm global.

It is also necessary to call await matrixClient.initCrypto() after creating a new MatrixClient (but before calling matrixClient.startClient()) to initialise the crypto layer.

If the Olm global is not available, the SDK will show a warning, as shown below; initCrypto() will also fail.

Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined

If the crypto layer is not (successfully) initialised, the SDK will continue to work for unencrypted rooms, but it will not support the E2E parts of the Matrix specification.

To provide the Olm library in a browser application:

  • download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
  • load olm.js as a <script> before browser-matrix.js.

To provide the Olm library in a node.js application:

  • yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz (replace the URL with the latest version you want to use from https://packages.matrix.org/npm/olm/)
  • global.Olm = require('olm'); before loading matrix-js-sdk.

If you want to package Olm as dependency for your node.js application, you can use yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz. If your application also works without e2e crypto enabled, add --optional to mark it as an optional dependency.

Contributing

This section is for people who want to modify the SDK. If you just want to use this SDK, skip this section.

First, you need to pull in the right build tools:

 $ yarn install

Building

To build a browser version from scratch when developing:

 $ yarn build

To run tests (Jest):

 $ yarn test

To run linting:

 $ yarn lint

Browser Storage Notes

Overview

Browsers examined: Firefox 67, Chrome 75

The examination below applies to the default, non-persistent storage policy.

Quota Measurement

Browsers appear to enforce and measure the quota in terms of space on disk, not data stored, so you may be able to store more data than the simple sum of all input data depending on how compressible your data is.

Quota Limit

Specs and documentation suggest we should consistently receive QuotaExceededError when we're near space limits, but the reality is a bit blurrier.

When we are low on disk space overall or near the group limit / origin quota:

  • Chrome
    • Log database may fail to start with AbortError
    • IndexedDB fails to start for crypto: AbortError in connect from indexeddb-store-worker
    • When near the quota, QuotaExceededError is used more consistently
  • Firefox
    • The first error will be QuotaExceededError
    • Future write attempts will fail with various errors when space is low, including nonsense like "InvalidStateError: A mutation operation was attempted on a database that did not allow mutations."
    • Once you start getting errors, the DB is effectively wedged in read-only mode
    • Can revive access if you reopen the DB

Cache Eviction

While the Storage Standard says all storage for an origin group should be limited by a single quota, in practice, browsers appear to handle localStorage separately from the others, so it has a separate quota limit and isn't evicted when low on space.

  • Chrome, Firefox
    • IndexedDB for origin deleted
    • Local Storage remains in place

Persistent Storage

Storage Standard offers a navigator.storage.persist API that can be used to request persistent storage that won't be deleted by the browser because of low space.

  • Chrome
  • Firefox
    • Firefox 67 shows a prompt to grant
    • Reverting persistent seems to require revoking permission and clearing site data

Storage Estimation

Storage Standard offers a navigator.storage.estimate API to get some clue of how much space remains.

  • Chrome, Firefox
    • Can run this at any time to request an estimate of space remaining
  • Firefox
    • Returns 0 for usage if a site is persisted

Random notes from Matthew on the two possible approaches for warning users about unexpected unverified devices popping up in their rooms....

Original idea...

Warn when an existing user adds an unknown device to a room.

Warn when a user joins the room with unverified or unknown devices.

Warn when you initial sync if the room has any unverified devices in it. ^ this is good enough if we're doing local storage. OR, better: Warn when you initial sync if the room has any new undefined devices since you were last there. => This means persisting the rooms that devices are in, across initial syncs.

Updated idea...

Warn when the user tries to send a message:

  • If the room has unverified devices which the user has not yet been told about in the context of this room ...or in the context of this user? currently all verification is per-user, not per-room. ...this should be good enough.

  • so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned. throw an error when trying to encrypt if there are pure unverified devices there app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?

    • or megolm could warn which devices are causing the problems.

Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?

Build Static Analysis Localazy Quality Gate Status Vulnerabilities Bugs

Element Desktop

Element Desktop is a Matrix client for desktop platforms with Element Web at its core.

First Steps

Before you do anything else, fetch the dependencies:

yarn install

Fetching Element

Since this package is just the Electron wrapper for Element Web, it doesn't contain any of the Element Web code, so the first step is to get a working copy of Element Web. There are a few ways of doing this:

# Fetch the prebuilt release Element package from the element-web GitHub releases page. The version
# fetched will be the same as the local element-desktop package.
# We're explicitly asking for no config, so the packaged Element will have no config.json.
yarn run fetch --noverify --cfgdir ""

...or if you'd like to use GPG to verify the downloaded package:

# Fetch the Element public key from the element.io web server over a secure connection and import
# it into your local GPG keychain (you'll need GPG installed). You only need to to do this
# once.
yarn run fetch --importkey
# Fetch the package and verify the signature
yarn run fetch --cfgdir ""

...or either of the above, but fetching a specific version of Element:

# Fetch the prebuilt release Element package from the element-web GitHub releases page. The version
# fetched will be the same as the local element-desktop package.
yarn run fetch --noverify --cfgdir "" v1.5.6

If you only want to run the app locally and don't need to build packages, you can provide the webapp directory directly:

# Assuming you've checked out and built a copy of element-web in ../element-web
ln -s ../element-web/webapp ./

[TODO: add support for fetching develop builds, arbitrary URLs and arbitrary paths]

Building

Native Build

TODO: List native pre-requisites

Optionally, build the native modules, which include support for searching in encrypted rooms and secure storage. Skipping this step is fine, you just won't have those features.

Then, run

yarn run build

This will do a couple of things:

  • Run the setversion script to set the local package version to match whatever version of Element you installed above.
  • Run electron-builder to build a package. The package built will match the operating system you're running the build process on.

Docker

Alternatively, you can also build using docker, which will always produce the linux package:

# Run this once to make the docker image
yarn run docker:setup

yarn run docker:install
# if you want to build the native modules (this will take a while)
yarn run docker:build:native
yarn run docker:build

After running, the packages should be in dist/.

Starting

If you'd just like to run the electron app locally for development:

yarn start

Config

If you'd like the packaged Element to have a configuration file, you can create a config directory and place config.json in there, then specify this directory with the --cfgdir option to yarn run fetch, eg:

mkdir myconfig
cp /path/to/my/config.json myconfig/
yarn run fetch --cfgdir myconfig

The config dir for the official Element app is in element.io. If you use this, your app will auto-update itself using builds from element.io.

Profiles

To run multiple instances of the desktop app for different accounts, you can launch the executable with the --profile argument followed by a unique identifier, e.g element-desktop --profile Work for it to run a separate profile and not interfere with the default one.

Alternatively, a custom location for the profile data can be specified using the --profile-dir flag followed by the desired path.

User-specified config.json

  • %APPDATA%\$NAME\config.json on Windows
  • $XDG_CONFIG_HOME/$NAME/config.json or ~/.config/$NAME/config.json on Linux
  • ~/Library/Application Support/$NAME/config.json on macOS

In the paths above, $NAME is typically Element, unless you use --profile $PROFILE in which case it becomes Element-$PROFILE, or it is using one of the above created by a pre-1.7 install, in which case it will be Riot or Riot-$PROFILE.

You may also specify a different path entirely for the config.json file by providing the --config $YOUR_CONFIG_JSON_FILE to the process, or via the ELEMENT_DESKTOP_CONFIG_JSON environment variable.

Translations

To add a new translation, head to the translating doc.

For a developer guide, see the translating dev doc.

Report bugs & give feedback

If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.

To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it. Please note that this issue tracker is associated with the element-web repo, but is also applied to the code in this repo as well.

Native Node Modules

For some features, the desktop version of Element can make use of native Node modules. These allow Element to integrate with the desktop in ways that a browser cannot.

While native modules enable powerful new features, they must be complied for each operating system. For official Element releases, we will always build these modules from source to ensure we can trust the compiled output. In the future, we may offer a pre-compiled path for those who want to use these features in a custom build of Element without installing the various build tools required.

The process is automated by vector-im/element-builder when releasing.

Building

Install the pre-requisites for your system:

Then optionally, add seshat and dependencies to support search in E2E rooms.

Then, to build for an architecture selected automatically based on your system (recommended), run:

yarn run build:native

If you need to build for a specific architecture, see here.

Adding Seshat for search in E2E encrypted rooms

Seshat is a native Node module that adds support for local event indexing and full text search in E2E encrypted rooms.

Since Seshat is written in Rust, the Rust compiler and related tools need to be installed before installing Seshat itself. To install Rust please consult the official Rust documentation.

Seshat also depends on the SQLCipher library to store its data in encrypted form on disk. You'll need to install it via your OS package manager.

After installing the Rust compiler and SQLCipher, Seshat support can be added using yarn at the root of this project:

yarn add matrix-seshat

You will have to rebuild the native libraries against electron's version of node rather than your system node, using the electron-build-env tool. This is also needed to when pulling in changes to Seshat using yarn link.

yarn add electron-build-env

Recompiling Seshat itself can be done like so:

yarn run electron-build-env -- --electron 6.1.1 -- neon build matrix-seshat --release

Please make sure to include all the -- as well as the --release command line switch at the end. Modify your electron version accordingly depending on the version that is installed on your system.

After this is done the Electron version of Element can be run from the main folder as usual using:

yarn start

Statically linking libsqlcipher

On Windows & macOS we always statically link libsqlcipher for it is not generally available. On Linux by default we will use a system package, on debian & ubuntu this is libsqlcipher0, but this is problematic for some other packages, and we found that it may crashes for unknown reasons. By including SQLCIPHER_BUNDLED=1 in the build environment, the build scripts will fully statically link sqlcipher, including a static build of OpenSSL.

More info can be found at https://github.com/matrix-org/seshat/issues/102 and https://github.com/vector-im/element-web/issues/20926.

Compiling for specific architectures

macOS

On macOS, you can build universal native modules too:

yarn run build:native:universal

...or you can build for a specific architecture:

yarn run build:native --target x86_64-apple-darwin

or

yarn run build:native --target aarch64-apple-darwin

You'll then need to create a built bundle with the same architecture. To bundle a universal build for macOS, run:

yarn run build:universal

Windows

If you're on Windows, you can choose to build specifically for 32 or 64 bit:

yarn run build:32

or

yarn run build:64

Cross compiling

Compiling a module for a particular operating system (Linux/macOS/Windows) needs to be done on that operating system. Cross-compiling from a host OS for a different target OS may be possible, but we don't support this flow with Element dependencies at this time.

Switching between architectures

The native module build system keeps the different architectures separate, so you can keep native modules for several architectures at the same time and switch which are active using a yarn run hak copy command, passing the appropriate architectures. This will error if you haven't yet built those architectures. eg:

yarn run build:native --target x86_64-apple-darwin
# We've now built & linked into place native modules for Intel
yarn run build:native --target aarch64-apple-darwin
# We've now built Apple Silicon modules too, and linked them into place as the active ones

yarn run hak copy --target x86_64-apple-darwin
# We've now switched back to our Intel modules
yarn run hak copy --target x86_64-apple-darwin --target aarch64-apple-darwin
# Now our native modules are universal x86_64+aarch64 binaries

The current set of native modules are stored in .hak/hakModules, so you can use this to check what architecture is currently in place, eg:

$ lipo -info .hak/hakModules/keytar/build/Release/keytar.node
Architectures in the fat file: .hak/hakModules/keytar/build/Release/keytar.node are: x86_64 arm64

Windows

Requirements to build native modules

We rely on Github Actions windows-2022 plus a few extra utilities as per the workflow.

If you want to build native modules, make sure that the following tools are installed on your system.

Once installed make sure all those utilities are accessible in your PATH.

If you want to be able to build x86 targets from an x64 host install the right toolchain:

rustup toolchain install stable-i686-pc-windows-msvc
rustup target add i686-pc-windows-msvc

In order to load all the C++ utilities installed by Visual Studio you can run the following in a terminal window.

call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" amd64

You can replace amd64 with x86 depending on your CPU architecture.

The Desktop app is capable of self-updating on macOS and Windows. The update server base url is configurable as update_base_url in config.json and can be served by a static file host, CDN or object storage.

Currently all packaging & deployment is handled by Github actions

Windows

On Windows the update mechanism used is Squirrel.Windows and can be served by any compatible Squirrel server, such as https://github.com/Tiliq/squirrel-server

macOS

On macOS the update mechanism used is Squirrel.Mac using the newer JSON format as documented here.

Packaging nightlies

Element Desktop nightly builds are build automatically by the Github Actions workflow. The schedule is currently set for once a day at 9am UTC. It will deploy to packages.element.io upon completion.

Triggering a manual nightly build

Simply go to https://github.com/vector-im/element-desktop/actions/workflows/build_and_deploy.yaml

  1. Click Run workflow
  2. Feel free to make changes to the checkboxes depending on the circumstances
  3. Click the green Run workflow

Packaging releases

Don't do this for RCs! We don't build Element Desktop for RCs.

For releasing Element Desktop, we assume the following prerequisites:

  • a tag of element-desktop repo with the Element Desktop version to be released set in package.json.
  • an Element Web tarball published to GitHub with a matching version number.

Both of these are done automatically when you run the release automation.

The packaging is kicked off automagically for you when a Github Release for Element Desktop is published.

More detail on the github actions

We moved to Github Actions for the following reasons:

  1. Removing single point of failure
  2. Improving reliability
  3. Unblocking the packaging on a single individual
  4. Improving parallelism

The Windows builds are signed by SSL.com using their Cloud Key Adapter for eSigner. This allows us to use Microsoft's signtool to interface with eSigner and send them a hash of the exe along with credentials in exchange for a signed certificate which we attach onto all the relevant files.

The Apple builds are signed using standard code signing means and then notarised to appease GateKeeper.

The Linux builds are distributed via a signed reprepro repository.

The packages.element.io site is a public Cloudflare R2 bucket which is deployed to solely from Github Actions. The main bucket in R2 is packages-element-io which is a direct mapping of packages.element.io, we have a workflow which generates the index.html files there to imitate a public index which Cloudflare does not currently support. The reprepro database lives in packages-element-io-db. There is an additional pair of buckets of same name but appended with -test which can be used for testing, these land on https://packages-element-io-test.element.io/.

Debian/Ubuntu Distributions

We used to add a new distribution to match each Debian and Ubuntu release. As of April 2020, we have created a default distribution that everyone can use (since the packages have never differed by distribution anyway).

The distribution configuration lives in https://github.com/vector-im/packages.element.io/blob/master/debian/conf/distributions as a canonical source.

Configuration

All Element Web options documented here can be used as well as the following:


The app contains a configuration file specified at build time using these instructions. This config can be overwritten by the end using by creating a config.json file at the paths described here.

After changing the config, the app will need to be exited fully (including via the task tray) and re-started.


  1. update_base_url: Specifies the URL of the update server, see document.
  2. web_base_url: Specifies the Element Web URL when performing actions such as popout widget. Defaults to https://app.element.io/.