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
- Definition:
- 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
- Definition:
- 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
- Definition:
- 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 yourContent-Security-Policy
header, as the modern replacement forX-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.
- Install or update
node.js
so that yournode
is at least the current recommended LTS. - Install
yarn
if not present already. - Clone the repo:
git clone https://github.com/element-hq/element-web.git
. - Switch to the element-web directory:
cd element-web
. - Install the prerequisites:
yarn install
.- If you're using the
develop
branch, then it is recommended to set up a proper development environment (see Setting up a dev environment below). Alternatively, you can use https://develop.element.io - the continuous integration release of the develop branch.
- If you're using the
- Configure the app by copying
config.sample.json
toconfig.json
and modifying it. See the configuration docs for details. 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.
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
- Definition:
- 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
- Definition:
- 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
- Definition:
- 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 yourContent-Security-Policy
header, as the modern replacement forX-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.
- Install or update
node.js
so that yournode
is at least the current recommended LTS. - Install
yarn
if not present already. - Clone the repo:
git clone https://github.com/element-hq/element-web.git
. - Switch to the element-web directory:
cd element-web
. - Install the prerequisites:
yarn install
.- If you're using the
develop
branch, then it is recommended to set up a proper development environment (see Setting up a dev environment below). Alternatively, you can use https://develop.element.io - the continuous integration release of the develop branch.
- If you're using the
- Configure the app by copying
config.sample.json
toconfig.json
and modifying it. See the configuration docs for details. 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
- Download the latest version from https://github.com/element-hq/element-web/releases
- Untar the tarball on your web server
- Move (or symlink) the
element-x.x.x
directory to an appropriate name - Configure the correct caching headers in your webserver (see below)
- Configure the app by copying
config.sample.json
toconfig.json
and modifying it. See the configuration docs for details. - 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:
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" } } }
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"
: A very deprecated method of defining the connection information. These are the same values seen asdefault_hs_url
and (optionally)default_is_url
base_url
in thedefault_server_config
example, withdefault_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:
default_federate
: Whentrue
(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.default_country_code
: An optional ISO 3166 alpha2 country code (eg:GB
, the default) to use when showing phone number inputs.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"] } }
setting_defaults
: Optional configuration for settings which are not described by this document and support theconfig
level. This list is incomplete. For example:
These values will take priority over the hardcoded defaults for the settings. For a list of available settings, see Settings.tsx.{ "setting_defaults": { "MessageComposerInput.showStickersButton": false, "MessageComposerInput.showPollsButton": false } }
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.
default_theme
: Typically eitherlight
(the default) ordark
, 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.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.brand
: Optional name for the app. Defaults toElement
. This is used throughout the application in various strings/locations.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 thanmatrix.to
.desktop_builds
: Optional. Where the desktop builds for the application are, if available. This is explained in more detail down below.mobile_builds
: Optional. Likedesktop_builds
, except for the mobile apps. Also described in more detail down below.mobile_guide_toast
: Whentrue
(default), users accessing the Element Web instance from a mobile device will be prompted to download the app instead.update_base_url
: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory containingmacos
andwin32
directories, with the update packages within. Defaults tohttps://packages.element.io/desktop/update/
in production.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 keym.tile_server
(or the unstable versionorg.matrix.msc3488.tile_server
). In this case, the configuration found in the well-known location is used instead.welcome_user_id
: DEPRECATED An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).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.branding
: Options for configuring various assets used within the app. Described in more detail down below.embedded_pages
: Further optional URLs for various assets used within the app. Described in more detail down below.disable_3pid_login
: Whenfalse
(default), enables the options to log in with email address or phone number. Set totrue
to hide these options.disable_login_language_selector
: Whenfalse
(default), enables the language selector on the login pages. Set totrue
to hide this dropdown.disable_guests
: Whenfalse
(default), enable guest-related functionality (peeking/previewing rooms, etc) for unregistered users. Set totrue
to disable this functionality.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:title
: Required. Title to show at the top of the notice.description
: Required. The description to use for the notice.show_once
: Optional. If true then the notice will only be shown once per device.
help_url
: The URL to point users to for help with the app, defaults tohttps://element.io/help
.help_encryption_url
: The URL to point users to for help with encryption, defaults tohttps://element.io/help#encryption
.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:
available
: Required. Whentrue
, the desktop app can be downloaded from somewhere.logo
: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.url
: Required. The download URL for the app. This is used as a hyperlink.url_macos
: Optional. Direct link to download macOS desktop app.url_win32
: Optional. Direct link to download Windows 32-bit desktop app.url_win64
: Optional. Direct link to download Windows 64-bit desktop app.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:
ios
: The URL for where to download the iOS app, such as an App Store link. When explicitlynull
, the app will assume the iOS app cannot be downloaded. When not provided, the default Element app will be assumed available.android
: The same asios
, except for Android instead.fdroid
: The same asandroid
, 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:
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.auth_header_logo_url
: A URL to the logo used on the login, registration, etc pages.auth_footer_links
: A list of links to add to the footer during login, registration, etc. Each entry must have atext
andurl
property.
embedded_pages
can be configured as such:
welcome_url
: A URL to an HTML page to show as a welcome page (landing on#/welcome
). When not specified, the defaultwelcome.html
that ships with Element will be used instead.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.login_for_welcome
: Whentrue
(defaultfalse
), the app will use the login form as a welcome page instead of the welcome page itself. This disables use ofwelcome_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:
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.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:
It is most common to use the{ "sso_redirect_options": { "immediate": false, "on_welcome_page": true, "on_login_page": true } }
immediate
flag instead ofon_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:
client_uri
: This is the base URI for the OIDC client registration, typicallylogo_uri
,tos_uri
, andpolicy_uri
must be either on the same domain or a subdomain of this URI.logo_uri
: Optional URI for the client logo.tos_uri
: Optional URI for the client's terms of service.policy_uri
: Optional URI for the client's privacy policy.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:
jitsi
: Optional configuration for how to start Jitsi conferences. Currently can only contain a singlepreferred_domain
value which points at the domain of the Jitsi instance. Defaults tomeet.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" } }
jitsi_widget
: Optional configuration for the built-in Jitsi widget. Currently can only contain a singleskip_built_in_welcome_screen
value, denoting whether the "Join Conference" button should be shown. Whentrue
(defaultfalse
), 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 } }
voip
: Optional configuration for various VoIP features. Currently can only contain a singleobey_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 tofalse
. For example:{ "voip": { "obey_asserted_identity": false } }
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 aroomId
query parameter to identify the room being called in. The URL must respond with a JSON object similar to the following:
The{ "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 } }
widget
is thecontent
of a normal widget state event. Thelayout
is the layout specifier for the widget being created, as defined by theio.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 optionwidget_build_url_ignore_dm
totrue
.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.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 tohttps://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 tofalse
.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 toElement 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:
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 tohttps://element.io/bugreports/submit
to submit rageshakes to us, or use your own rageshake server.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 beelement-auto-uisi
(in contrast to other rageshakes submitted by the app, which useelement-web
).existing_issues_url
: URL for where to find existing issues.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:
dsn
: The Sentry DSN.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
.
integrations_ui_url
: The UI URL for the integration manager.integrations_rest_url
: The REST interface URL for the integration manager.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:
api_host
: The hostname of the posthog server.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:
analytics_owner
: the company name used in dialogs talking about analytics - this defaults tobrand
, and is useful when the provider of analytics is different from the provider of the Element instance.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:
- 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.
- The identity server provided by the
.well-known
lookup that occurred at login - 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.
fallback_hs_url
sync_timeline_limit
dangerously_allow_unsafe_and_insecure_passwords
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 whenfeature_latex_maths
is enabled.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 tofalse
.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:
- Copy the default customisation module to
element-web/src/customisations/YourNameMedia.ts
- Edit customisations points and make sure export the ones you actually want to activate
- 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:
- The module must depend on
@matrix-org/react-sdk-module-api
(usually as a dev dependency). - The module's
main
entrypoint must have adefault
export for theRuntimeModule
instance, supporting a constructor which takes a single parameter: aModuleApi
instance. This instance is passed tosuper()
. - 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
orX-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
- Come and join https://matrix.to/#/#element-translations:matrix.org for general discussion
- Join https://matrix.to/#/#element-translators:matrix.org for language-specific rooms
- 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.
What are <link>Something</link>
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.
- Ensure the string doesn't already exist in a related project, such as https://localazy.com/p/element
- Keys for common strings, i.e. strings that can be used at multiple places must start by
action_
if this is a verb, orcommon_
if not - Keys for common accessibility strings must start by
a11y_
. Example:a11y_hide_password
- Otherwise, try to group keys logically and nest where appropriate, such as
keyboard_
for strings relating to keyboard shortcuts. - Ensure your translation keys do not include
.
or|
or
Adding new strings
- Check if the import
import { _t } from ".../languageHandler";
is present. If not add it to the other import statements. Also import_td
if needed. - 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. - Run
yarn i18n
to add the keys tosrc/i18n/strings/en_EN.json
- 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.
- Extend your
_t()
call. Instead of_t(TKEY)
use_t(TKEY, {})
- 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.
- Add it to the array in
_t
for example_t(TKEY, {variable: this.variable})
- 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 incount
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()
insidegetDefaultProps
: the translations aren't loaded whengetDefaultProps
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:
- Pick a name, e.g.
teal
. at time of writing we havelight
anddark
. - Fork
res/themes/dark/css/dark.pcss
to beteal.pcss
- Fork
res/themes/dark/css/_base.pcss
to be_teal.pcss
- 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. - Add the theme to the list of entrypoints in webpack.config.js
- Add the theme to the list of themes in theme.ts
- 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 securityFORM_SECRET
: Generated each time for securityPUBLIC_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.
- Test a well-isolated unit of functionality. The more specific, the easier it will be to tell what's wrong when they fail.
- Don't depend on state from other tests: any given test should be able to run in isolation.
- 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.
- 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)
- Press CTRL+SHIFT+I (I as in eye).
- Click the Memory tab.
- Press the camera icon in the top left of the pane.
- Wait a bit (coffee is a good option).
- When the save button appears on the left side of the panel, click it to save the profile locally.
- Compress the file (gzip or regular zip) to make the file smaller.
- 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)
- Press CTRL+SHIFT+I (I as in eye).
- Click the Memory tab.
- Select "Heap Snapshot" and the app.element.io VM instance (not the indexeddb one).
- Click "Take Snapshot".
- Wait a bit (coffee is a good option).
- When the save button appears on the left side of the panel, click it to save the profile locally.
- Compress the file (gzip or regular zip) to make the file smaller.
- 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:
- Add a new setting of the form:
"feature_cats": {
isFeature: true,
displayName: _td("Adds cats everywhere"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
- Check whether the feature is enabled as appropriate:
SettingsStore.getValue("feature_cats");
- 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:
- Remove
isFeature
from the setting - Change the
default
totrue
(if desired). - Remove the feature from the labs documentation
- Celebrate! 🥳
If the feature is meant to be forced on (non-configurable):
- Remove the setting
- Remove all
getValue
lines that test for the feature. - Remove the feature from the labs documentation
- If applicable, remove the feature state from develop, nightly, staging / app, and release configs
- 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.
- Update topics using the automation. It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work.
- Announce the release in #element-web-announcements:matrix.org
(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:
-
Go to the matrix-js-sdk Renovate dashboard and click the checkbox to create/update its PRs.
-
Go to the element-web Renovate dashboard and click the checkbox to create/update its PRs.
-
Go to the element-desktop Renovate dashboard and click the checkbox to create/update its PRs.
-
Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the maintainer.
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 tofalse
.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 EchoTransaction
s. 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 EchoTransaction
s
and EchoContext
s - 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-- needs -->ID5 ID6[["Typescript Syntax Check"]] click ID6 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow" ID7[["ESLint"]] click ID7 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow" ID8[["Workflow Lint"]] click ID8 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow" ID9[["JSDoc Checker"]] click ID9 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow" IDa[["Analyse Dead Code"]] click IDa href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow" ID4[["Downstream tsc element-web"]] click ID4 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow" ID5[["Downstream Typescript Syntax Check"]] click ID5 href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/static_analysis.yml" "Click to open workflow" end IDb(("Pull Request<br>matrix-js-sdk")) subgraph IDc["Tests"] IDd-- needs -->IDe IDf-- needs -->IDg IDh-- needs -->IDi subgraph IDd["Jest [)"] IDj[["Jest [integ] (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 [integ] (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" IDl[["Jest [unit] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"]] click IDl href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow" IDm[["Jest [unit] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"]] click IDm href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow" end IDe[["Jest tests"]] click IDe href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow" IDh[["Downstream test element-web"]] click IDh href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow" IDf[["Run Complement Crypto tests"]] click IDf href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow" IDg[["Downstream Complement Crypto tests"]] click IDg href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow" IDi[["Downstream tests"]] click IDi href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/tests.yml" "Click to open workflow" end subgraph IDn["SonarQube"] IDo[["🩻 SonarQube"]] click IDo href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/sonarqube.yml" "Click to open workflow" end IDp(("Push matrix-js-sdk<br>develop")) subgraph IDq["Sync labels"] IDr[["sync-labels"]] click IDr href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/sync-labels.yml" "Click to open workflow" end IDs(("At 01:00 AM")) subgraph IDt["Notify Downstream Projects"] subgraph IDu["notify-downstream"] IDv[["notify-downstream (element-hq/element-web, element-web-notify)"]] click IDv href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/notify-downstream.yaml" "Click to open workflow" end end subgraph IDw["matrix-react-sdk End to End Tests"] IDx[["Playwright"]] click IDx 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 IDy["Deploy documentation PR preview"] IDz[["netlify"]] click IDz href "https://github.com/matrix-org/matrix-js-sdk/blob/develop/.github/workflows/docs-pr-netlify.yaml" "Click to open workflow" end ID10(("Manual")) ID2 --> ID0 ID2 --> ID3 ID2 --> IDc IDb --> ID3 IDp --> ID3 ID3-- workflow_run -->IDy IDb --> IDc IDb --> IDw IDc-- workflow_run -->IDn IDp --> IDc IDp --> IDq IDp --> IDt IDs --> IDq ID10 --> IDq
--- 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.35s.
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:
- Windows pre-requisites
- Linux: TODO
- OS X: TODO
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.
- Git for Windows
- Node 16
- Python 3 (if you type 'python' into command prompt it will offer to install it from the windows store)
- Strawberry Perl
- Rustup
- NASM
- Build Tools for Visual Studio 2019 with the following configuration:
- On the Workloads tab:
- Desktop & Mobile -> C++ build tools
- On the Individual components tab:
- MSVC VS 2019 C++ build tools
- Windows 10 SDK (latest version available)
- C++ CMake tools for Windows
- On the Workloads tab:
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
- Click
Run workflow
- Feel free to make changes to the checkboxes depending on the circumstances
- 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 inpackage.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:
- Removing single point of failure
- Improving reliability
- Unblocking the packaging on a single individual
- 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.
update_base_url
: Specifies the URL of the update server, see document.web_base_url
: Specifies the Element Web URL when performing actions such as popout widget. Defaults tohttps://app.element.io/
.
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 enable end-to-end-encryption, or 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 /download
ing or /thumbnail
ing 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
andUsers
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
EventEmitter
s. 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 point | Description |
---|---|
matrix-js-sdk | Primary entry point. High-level functionality, and lots of historical clutter in need of a cleanup. |
matrix-js-sdk/lib/crypto-api | Cryptography functionality. |
matrix-js-sdk/lib/types | Low-level types, reflecting data structures defined in the Matrix spec. |
matrix-js-sdk/lib/testing | Test utilities, which may be useful in test code but should not be used in production code. |
matrix-js-sdk/lib/utils/*.js | A 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();
Print out messages for all rooms
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
Print out membership lists whenever they are changed
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
matrix-js-sdk
's end-to-end encryption support is based on the WebAssembly bindings of the Rust matrix-sdk-crypto library.
Initialization
Do not use matrixClient.initCrypto()
. This method is deprecated and no longer maintained.
To initialize the end-to-end encryption support in the matrix client:
// Create a new matrix client
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId,
});
// Initialize to enable end-to-end encryption support.
await matrixClient.initRustCrypto();
After calling initRustCrypto
, you can obtain a reference to the CryptoApi
interface, which is the main entry point for end-to-end encryption, by calling MatrixClient.getCrypto
.
Secret storage
You should normally set up secret storage before using the end-to-end encryption. To do this, call CryptoApi.bootstrapSecretStorage
.
bootstrapSecretStorage
can be called unconditionally: it will only set up the secret storage if it is not already set up (unless you use the setupNewSecretStorage
parameter).
const matrixClient = sdk.createClient({
...,
cryptoCallbacks: {
getSecretStorageKey: async (keys) => {
// This function should prompt the user to enter their secret storage key.
return mySecretStorageKeys;
},
},
});
matrixClient.getCrypto().bootstrapSecretStorage({
// This function will be called if a new secret storage key (aka recovery key) is needed.
// You should prompt the user to save the key somewhere, because they will need it to unlock secret storage in future.
createSecretStorageKey: async () => {
return mySecretStorageKey;
},
});
The example above will create a new secret storage key if secret storage was not previously set up.
The secret storage data will be encrypted using the secret storage key returned in createSecretStorageKey
.
We recommend that you prompt the user to re-enter this key when CryptoCallbacks.getSecretStorageKey
is called (when the secret storage access is needed).
Set up cross-signing
To set up cross-signing to verify devices and other users, call
CryptoApi.bootstrapCrossSigning
:
matrixClient.getCrypto().bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
return makeRequest(authDict);
},
});
The authUploadDeviceSigningKeys
callback is required in order to upload newly-generated public cross-signing keys to the server.
Key backup
If the user doesn't already have a key backup you should create one:
// Check if we have a key backup.
// If checkKeyBackupAndEnable returns null, there is no key backup.
const hasKeyBackup = (await matrixClient.getCrypto().checkKeyBackupAndEnable()) !== null;
// Create the key backup
await matrixClient.getCrypto().resetKeyBackup();
Verify a new device
Once the cross-signing is set up on one of your devices, you can verify another device with two methods:
-
Use
CryptoApi.bootstrapCrossSigning
.bootstrapCrossSigning
will call the CryptoCallbacks.getSecretStorageKey callback. The device is verified with the private cross-signing keys fetched from the secret storage. -
Request an interactive verification against existing devices, by calling CryptoApi.requestOwnUserVerification.
Migrating from the legacy crypto stack to Rust crypto
If your application previously used the legacy crypto stack, (i.e, it called MatrixClient.initCrypto()
), you will
need to migrate existing devices to the Rust crypto stack.
This migration happens automatically when you call initRustCrypto()
instead of initCrypto()
,
but you need to provide the legacy cryptoStore
and pickleKey
to createClient
:
// You should provide the legacy crypto store and the pickle key to the matrix client in order to migrate the data.
const matrixClient = sdk.createClient({
cryptoStore: myCryptoStore,
pickleKey: myPickleKey,
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId,
});
// The migration will be done automatically when you call `initRustCrypto`.
await matrixClient.initRustCrypto();
To follow the migration progress, you can listen to the CryptoEvent.LegacyCryptoStoreMigrationProgress
event:
// When progress === total === -1, the migration is finished.
matrixClient.on(CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress, total) => {
...
});
The Rust crypto stack is not supported in a lot of deprecated methods of MatrixClient
. If you use them, you should migrate to the CryptoApi
. Also, the legacy MatrixClient.crypto
object is not available any more: you should use MatrixClient.getCrypto()
instead.
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
- Chrome 75 seems to grant this without any prompt based on interaction criteria
- 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
forusage
if a site is persisted
- Returns
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?