Merge branch 'develop' into bwindels/settings-config-doc

This commit is contained in:
Bruno Windels 2020-04-01 11:43:03 +02:00
commit 66aea5eea5
947 changed files with 78891 additions and 33605 deletions

71
docs/ciderEditor.md Normal file
View file

@ -0,0 +1,71 @@
# The CIDER (Contenteditable-Input-Diff-Error-Reconcile) editor
The CIDER editor is a custom editor written for Riot.
Most of the code can be found in the `/editor/` directory of the `matrix-react-sdk` project.
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.js`.
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.js` 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.js` 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.

6
docs/features/README.md Normal file
View file

@ -0,0 +1,6 @@
# Feature documention
The idea of this folder is to document the features we support in different parts of the app.
In case anyone needs to work on a given part, and isn't aware of all the features in the area,
they will hopefully get an idea for all the supported functionality to know what to take into account
when making changes.

37
docs/features/composer.md Normal file
View file

@ -0,0 +1,37 @@
# Composer Features
## Auto Complete
- Hitting tab tries to auto-complete the word before the caret as a room member
- If no matching name is found, a visual bell is shown
- @ + a letter opens auto complete for members starting with the given letter
- When inserting a user pill at the start in the composer, a colon and space is appended to the pill
- When inserting a user pill anywhere else in composer, only a space is appended to the pill
- # + a letter opens auto complete for rooms starting with the given letter
- : open auto complete for emoji
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options
- Pressing tab while the autocomplete is open goes to the next autocomplete option,
wrapping around at the end after reverting to the typed text first.
## Formatting
- When selecting text, a formatting bar appears above the selection.
- The formatting bar allows to format the selected test as:
bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected).
- Formatting is applied as markdown syntax.
- Hitting ctrl/cmd+B also marks the selected text as bold
- Hitting ctrl/cmd+I also marks the selected text as italic
- Hitting ctrl/cmd+> also marks the selected text as a blockquote
## Misc
- When hitting the arrow-up button while having the caret at the start in the composer,
the last message sent by the syncing user is edited.
- Clicking a display name on an event in the timeline inserts a user pill into the composer
- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
- Typing in the composer sends typing notifications in the room
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
- Pressing shift+enter inserts a line break
- Pressing enter sends the message.
- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer.
- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to.
- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.

31
docs/jitsi.md Normal file
View file

@ -0,0 +1,31 @@
# Jitsi Wrapper
**Note**: These are developer docs. Please consult your client's documentation for
instructions on setting up Jitsi.
The react-sdk wraps all Jitsi call widgets in a local wrapper called `jitsi.html`
which takes several parameters:
*Query string*:
* `widgetId`: The ID of the widget. This is needed for communication back to the
react-sdk.
* `parentUrl`: The URL of the parent window. This is also needed for
communication back to the react-sdk.
*Hash/fragment (formatted as a query string)*:
* `conferenceDomain`: The domain to connect Jitsi Meet to.
* `conferenceId`: The room or conference ID to connect Jitsi Meet to.
* `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
be present, should default to `false`.
* `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://riot.im/develop/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.

28
docs/scrolling.md Normal file
View file

@ -0,0 +1,28 @@
# 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/riot-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](https://wiki.mozilla.org/Platform/GFX/APZ) 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.

View file

@ -15,7 +15,7 @@ order of prioirty, are:
* `room-account` - The current user's account, but only when in a specific room
* `account` - The current user's account
* `room` - A specific room (setting for all members of the room)
* `config` - Values are defined by `config.json`
* `config` - Values are defined by the `settingDefaults` key (usually) in `config.json`
* `default` - The hardcoded default for the settings
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure

71
docs/skinning.md Normal file
View file

@ -0,0 +1,71 @@
# Skinning
The react-sdk can be skinned to replace presentation components, CSS, or
other relevant parts of the SDK. Typically consumers will replace entire
components and get the ability for custom CSS as a result.
This doc isn't exhaustive on how skinning works, though it should cover
some of the more complicated parts such as component replacement.
## Loading a skin
1. Generate a `component-index.js` (preferably using the tools that the react-sdk
exposes). This can typically be done with a npm script like `"reskindex -h src/header"`.
2. In your app's entry point, add something like this code:
```javascript
import {loadSkin} from "matrix-react-sdk";
loadSkin(import("component-index").components);
// The rest of your imports go under this.
```
3. Import the remainder of the SDK and bootstrap your app.
It is extremely important that you **do not** import anything else from the
SDK prior to loading your skin as otherwise the skin might not work. Loading
the skin should be one of the first things your app does, if not the very
first thing.
Additionally, **do not** provide `loadSkin` with the react-sdk components
themselves otherwise the app might explode. The SDK is already aware of its
components and doesn't need to be told.
## Replacing components
Components that replace the react-sdk ones MUST have a `replaces` static
key on the component's class to describe which component it overrides. For
example, if your `VectorAuthPage` component is meant to replace the react-sdk
`AuthPage` component then you'd add `static replaces = 'views.auth.AuthPage';`
to the `VectorAuthPage` class.
Other than that, the skin just needs to be loaded normally as mentioned above.
Consumers of the SDK likely will not be interested in the rest of this section.
### SDK developer notes
Components in the react-sdk MUST be decorated with the `@replaceableComponent`
function. For components that can't use the decorator, they must use a
variation that provides similar functionality. The decorator gives consumers
an opportunity to load skinned components by abusing import ordering and
behaviour.
Decorators are executed at import time which is why we can abuse the import
ordering behaviour: importing `loadSkin` doesn't trigger any components to
be imported, allowing the consumer to specify a skin. When the consumer does
import a component (for example, `MatrixChat`), it starts to pull in all the
components via `import` statements. When the components get pulled in the
decorator checks with the skinned components to see if it should be replacing
the component being imported. The decorator then effectively replaces the
components when needed by specifying the skinned component as an override for
the SDK's component, which should in theory override critical functions like
`render()` and lifecycle event handlers.
The decorator also means that older usage of `getComponent()` is no longer
required because components should be replaced by the decorator. Eventually
the react-sdk should only have one usage of `getComponent()`: the decorator.
The decorator assumes that if `getComponent()` returns null that there is
no skinned version of the component and continues on using the SDK's component.
In previous versions of the SDK, the function would throw an error instead
because it also expected the skin to list the SDK's components as well, however
that is no longer possible due to the above.
In short, components should always be `import`ed.

27
docs/usercontent.md Normal file
View file

@ -0,0 +1,27 @@
# 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 riot 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 riot 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:
```json5
{
"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 riot-web's webpack config.