Merge branch 'develop' into jaywink/hosting-provider-iframe-minimize-wip

pull/21833/head
Jason Robinson 2021-02-03 22:35:22 +02:00
commit 6ccce7142c
156 changed files with 7020 additions and 5218 deletions

View File

@ -22,6 +22,8 @@ module.exports = {
"files": ["src/**/*.{ts,tsx}"],
"extends": ["matrix-org/ts"],
"rules": {
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do

View File

@ -1,3 +1,185 @@
Changes in [3.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0) (2021-02-03)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0-rc.1...v3.13.0)
* Upgrade to JS SDK 9.6.0
* [Release] Fix flair height after accent changes
[\#5612](https://github.com/matrix-org/matrix-react-sdk/pull/5612)
* [Release] Iterate Social Logins work around edge cases and branding
[\#5610](https://github.com/matrix-org/matrix-react-sdk/pull/5610)
* [Release] Lock widget room ID when added
[\#5608](https://github.com/matrix-org/matrix-react-sdk/pull/5608)
* [Release] Better errors for SSO failures
[\#5606](https://github.com/matrix-org/matrix-react-sdk/pull/5606)
* [Release] Fix RoomView re-mounting breaking peeking
[\#5603](https://github.com/matrix-org/matrix-react-sdk/pull/5603)
Changes in [3.13.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0-rc.1) (2021-01-29)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.1...v3.13.0-rc.1)
* Upgrade to JS SDK 9.6.0-rc.1
* Translations update from Weblate
[\#5597](https://github.com/matrix-org/matrix-react-sdk/pull/5597)
* Support managed hybrid widgets from config
[\#5596](https://github.com/matrix-org/matrix-react-sdk/pull/5596)
* Add managed hybrid call widgets when supported
[\#5594](https://github.com/matrix-org/matrix-react-sdk/pull/5594)
* Tweak mobile guide toast copy
[\#5595](https://github.com/matrix-org/matrix-react-sdk/pull/5595)
* Improve SSO auth flow
[\#5578](https://github.com/matrix-org/matrix-react-sdk/pull/5578)
* Add optional mobile guide toast
[\#5586](https://github.com/matrix-org/matrix-react-sdk/pull/5586)
* Fix invisible text after logging out in the dark theme
[\#5588](https://github.com/matrix-org/matrix-react-sdk/pull/5588)
* Fix escape for cancelling replies
[\#5591](https://github.com/matrix-org/matrix-react-sdk/pull/5591)
* Update widget-api to beta.12
[\#5589](https://github.com/matrix-org/matrix-react-sdk/pull/5589)
* Add commands for DM conversion
[\#5540](https://github.com/matrix-org/matrix-react-sdk/pull/5540)
* Run a UI refresh over the OIDC Exchange confirmation dialog
[\#5580](https://github.com/matrix-org/matrix-react-sdk/pull/5580)
* Allow stickerpickers the legacy "visibility" capability
[\#5581](https://github.com/matrix-org/matrix-react-sdk/pull/5581)
* Hide local video if it is muted
[\#5529](https://github.com/matrix-org/matrix-react-sdk/pull/5529)
* Don't use name width in reply thread for IRC layout
[\#5518](https://github.com/matrix-org/matrix-react-sdk/pull/5518)
* Update code_style.md
[\#5554](https://github.com/matrix-org/matrix-react-sdk/pull/5554)
* Fix Czech capital letters like ŠČŘ...
[\#5569](https://github.com/matrix-org/matrix-react-sdk/pull/5569)
* Add optional search shortcut
[\#5548](https://github.com/matrix-org/matrix-react-sdk/pull/5548)
* Fix Sudden 'find a room' UI shows up when the only room moves to favourites
[\#5584](https://github.com/matrix-org/matrix-react-sdk/pull/5584)
* Increase PersistedElement's z-index
[\#5568](https://github.com/matrix-org/matrix-react-sdk/pull/5568)
* Remove check that prevents Jitsi widgets from being unpinned
[\#5582](https://github.com/matrix-org/matrix-react-sdk/pull/5582)
* Fix Jitsi widgets causing localized tile crashes
[\#5583](https://github.com/matrix-org/matrix-react-sdk/pull/5583)
* Log candidates for calls
[\#5573](https://github.com/matrix-org/matrix-react-sdk/pull/5573)
* Upgrade deps 2021-01
[\#5579](https://github.com/matrix-org/matrix-react-sdk/pull/5579)
* Fix "Continuing without email" dialog bug
[\#5566](https://github.com/matrix-org/matrix-react-sdk/pull/5566)
* Require registration for verification actions
[\#5574](https://github.com/matrix-org/matrix-react-sdk/pull/5574)
* Don't play the hangup sound when the call is answered from elsewhere
[\#5572](https://github.com/matrix-org/matrix-react-sdk/pull/5572)
* Move to newer base image for end-to-end tests
[\#5570](https://github.com/matrix-org/matrix-react-sdk/pull/5570)
* Update widgets in the room upon join
[\#5564](https://github.com/matrix-org/matrix-react-sdk/pull/5564)
* Update AuxPanel and related buttons when widgets change or on reload
[\#5563](https://github.com/matrix-org/matrix-react-sdk/pull/5563)
* Add VoIP user mapper
[\#5560](https://github.com/matrix-org/matrix-react-sdk/pull/5560)
* Improve styling of SSO Buttons for multiple IdPs
[\#5558](https://github.com/matrix-org/matrix-react-sdk/pull/5558)
* Fixes for the general tab in the room dialog
[\#5522](https://github.com/matrix-org/matrix-react-sdk/pull/5522)
* fix issue 16226 to allow switching back to default HS.
[\#5561](https://github.com/matrix-org/matrix-react-sdk/pull/5561)
* Support room-defined widget layouts
[\#5553](https://github.com/matrix-org/matrix-react-sdk/pull/5553)
* Change a bunch of strings from Recovery Key/Phrase to Security Key/Phrase
[\#5533](https://github.com/matrix-org/matrix-react-sdk/pull/5533)
* Give a bigger target area to AppsDrawer vertical resizer
[\#5557](https://github.com/matrix-org/matrix-react-sdk/pull/5557)
* Fix minimized left panel avatar alignment
[\#5493](https://github.com/matrix-org/matrix-react-sdk/pull/5493)
* Ensure component index has been written before renaming
[\#5556](https://github.com/matrix-org/matrix-react-sdk/pull/5556)
* Fixed continue button while selecting home-server
[\#5552](https://github.com/matrix-org/matrix-react-sdk/pull/5552)
* Wire up MSC2931 widget navigation
[\#5527](https://github.com/matrix-org/matrix-react-sdk/pull/5527)
* Various fixes for Bridge Info page (MSC2346)
[\#5454](https://github.com/matrix-org/matrix-react-sdk/pull/5454)
* Use room-specific listeners for message preview and community prototype
[\#5547](https://github.com/matrix-org/matrix-react-sdk/pull/5547)
* Fix some misc. React warnings when viewing timeline
[\#5546](https://github.com/matrix-org/matrix-react-sdk/pull/5546)
* Use device storage for allowed widgets if account data not supported
[\#5544](https://github.com/matrix-org/matrix-react-sdk/pull/5544)
* Fix incoming call box on dark theme
[\#5542](https://github.com/matrix-org/matrix-react-sdk/pull/5542)
* Convert DMRoomMap to typescript
[\#5541](https://github.com/matrix-org/matrix-react-sdk/pull/5541)
* Add in-call dialpad for DTMF sending
[\#5532](https://github.com/matrix-org/matrix-react-sdk/pull/5532)
Changes in [3.12.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.1) (2021-01-26)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0...v3.12.1)
* Upgrade to JS SDK 9.5.1
Changes in [3.12.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0) (2021-01-18)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0-rc.1...v3.12.0)
* Upgrade to JS SDK 9.5.0
* Fix incoming call box on dark theme
[\#5543](https://github.com/matrix-org/matrix-react-sdk/pull/5543)
Changes in [3.12.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0-rc.1) (2021-01-13)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.1...v3.12.0-rc.1)
* Upgrade to JS SDK 9.5.0-rc.1
* Fix soft crash on soft logout page
[\#5539](https://github.com/matrix-org/matrix-react-sdk/pull/5539)
* Translations update from Weblate
[\#5538](https://github.com/matrix-org/matrix-react-sdk/pull/5538)
* Run TypeScript tests
[\#5537](https://github.com/matrix-org/matrix-react-sdk/pull/5537)
* Add a basic widget explorer to devtools (per-room)
[\#5528](https://github.com/matrix-org/matrix-react-sdk/pull/5528)
* Add <input type="password"> to security key field
[\#5534](https://github.com/matrix-org/matrix-react-sdk/pull/5534)
* Fix avatar upload prompt/tooltip floating wrong and permissions
[\#5526](https://github.com/matrix-org/matrix-react-sdk/pull/5526)
* Add a dialpad UI for PSTN lookup
[\#5523](https://github.com/matrix-org/matrix-react-sdk/pull/5523)
* Basic call transfer initiation support
[\#5494](https://github.com/matrix-org/matrix-react-sdk/pull/5494)
* Fix #15988
[\#5524](https://github.com/matrix-org/matrix-react-sdk/pull/5524)
* Bump node-notifier from 8.0.0 to 8.0.1
[\#5520](https://github.com/matrix-org/matrix-react-sdk/pull/5520)
* Use TypeScript source for development, swap to build during release
[\#5503](https://github.com/matrix-org/matrix-react-sdk/pull/5503)
* Look for emoji in the body that will be displayed
[\#5517](https://github.com/matrix-org/matrix-react-sdk/pull/5517)
* Bump ini from 1.3.5 to 1.3.7
[\#5486](https://github.com/matrix-org/matrix-react-sdk/pull/5486)
* Recognise `*.element.io` links as Element permalinks
[\#5514](https://github.com/matrix-org/matrix-react-sdk/pull/5514)
* Fixes for call UI
[\#5509](https://github.com/matrix-org/matrix-react-sdk/pull/5509)
* Add a snowfall chat effect (with /snowfall command)
[\#5511](https://github.com/matrix-org/matrix-react-sdk/pull/5511)
* fireworks effect
[\#5507](https://github.com/matrix-org/matrix-react-sdk/pull/5507)
* Don't play call end sound for calls that never started
[\#5506](https://github.com/matrix-org/matrix-react-sdk/pull/5506)
* Add /tableflip slash command
[\#5485](https://github.com/matrix-org/matrix-react-sdk/pull/5485)
* Import from src in IncomingCallBox.tsx
[\#5504](https://github.com/matrix-org/matrix-react-sdk/pull/5504)
* Social Login support both https and mxc icons
[\#5499](https://github.com/matrix-org/matrix-react-sdk/pull/5499)
* Fix padding in confirmation email registration prompt
[\#5501](https://github.com/matrix-org/matrix-react-sdk/pull/5501)
* Fix room list help prompt alignment
[\#5500](https://github.com/matrix-org/matrix-react-sdk/pull/5500)
Changes in [3.11.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.1) (2020-12-21)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0...v3.11.1)

View File

@ -35,12 +35,6 @@ General Style
- lowerCamelCase for functions and variables.
- Single line ternary operators are fine.
- UPPER_SNAKE_CASE for constants
- Single quotes for strings by default, for consistency with most JavaScript styles:
```javascript
"bad" // Bad
'good' // Good
```
- Use parentheses or `` ` `` instead of `\` for line continuation where ever possible
- Open braces on the same line (consistent with Node):
@ -162,7 +156,14 @@ ECMAScript
- Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an
arrow function, they probably all should be.
- Apart from that, newer ES features should be used whenever the author deems them to be appropriate.
- Flow annotations are welcome and encouraged.
TypeScript
----------
- TypeScript is preferred over the use of JavaScript
- It's desirable to convert existing JavaScript files to TypeScript. TypeScript conversions should be done in small
chunks without functional changes to ease the review process.
- Use full type definitions for function parameters and return values.
- Avoid `any` types and `any` casts
React
-----
@ -201,6 +202,8 @@ React
this.state = { counter: 0 };
}
```
- Prefer class components over function components and hooks (not a strict rule though)
- Think about whether your component really needs state: are you duplicating
information in component state that could be derived from the model?

60
docs/widget-layouts.md Normal file
View File

@ -0,0 +1,60 @@
# 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:
```json5
{
"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.

View File

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "3.11.1",
"version": "3.13.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -54,48 +54,47 @@
"test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080"
},
"dependencies": {
"@babel/runtime": "^7.10.5",
"await-lock": "^2.0.1",
"blueimp-canvas-to-blob": "^3.27.0",
"@babel/runtime": "^7.12.5",
"await-lock": "^2.1.0",
"blueimp-canvas-to-blob": "^3.28.0",
"browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3",
"cheerio": "^1.0.0-rc.3",
"cheerio": "^1.0.0-rc.5",
"classnames": "^2.2.6",
"commonmark": "^0.29.1",
"commonmark": "^0.29.3",
"counterpart": "^0.18.6",
"diff-dom": "^4.1.6",
"diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5",
"emojibase-data": "^5.0.1",
"emojibase-regex": "^4.0.1",
"emojibase-data": "^5.1.1",
"emojibase-regex": "^4.1.1",
"escape-html": "^1.0.3",
"file-saver": "^1.3.8",
"filesize": "3.6.1",
"file-saver": "^2.0.5",
"filesize": "6.1.0",
"flux": "2.1.1",
"focus-visible": "^5.1.0",
"fuse.js": "^2.7.4",
"focus-visible": "^5.2.0",
"gfm.css": "^1.1.2",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^10.1.2",
"html-entities": "^1.3.1",
"is-ip": "^2.0.0",
"highlight.js": "^10.5.0",
"html-entities": "^1.4.0",
"is-ip": "^3.1.0",
"katex": "^0.12.0",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.19",
"lodash": "^4.17.20",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^0.1.0-beta.10",
"matrix-widget-api": "^0.1.0-beta.13",
"minimist": "^1.2.5",
"pako": "^1.0.11",
"parse5": "^5.1.1",
"pako": "^2.0.3",
"parse5": "^6.0.1",
"png-chunks-extract": "^1.0.0",
"project-name-generator": "^2.1.7",
"project-name-generator": "^2.1.9",
"prop-types": "^15.7.2",
"qrcode": "^1.4.4",
"qs": "^6.9.4",
"re-resizable": "^6.5.4",
"react": "^16.13.1",
"qs": "^6.9.6",
"re-resizable": "^6.9.0",
"react": "^16.14.0",
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.13.1",
"react-focus-lock": "^2.4.1",
"react-dom": "^16.14.0",
"react-focus-lock": "^2.5.0",
"react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.1",
"rfc4648": "^1.4.0",
@ -108,68 +107,71 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.10.5",
"@babel/parser": "^7.11.0",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-export-default-from": "^7.10.4",
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.10.4",
"@babel/plugin-transform-flow-comments": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@babel/register": "^7.10.5",
"@babel/traverse": "^7.11.0",
"@peculiar/webcrypto": "^1.1.3",
"@types/classnames": "^2.2.10",
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/parser": "^7.12.11",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-decorators": "^7.12.12",
"@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-transform-flow-comments": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-flow": "^7.12.1",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@babel/traverse": "^7.12.12",
"@peculiar/webcrypto": "^1.1.4",
"@sinonjs/fake-timers": "^7.0.2",
"@types/classnames": "^2.2.11",
"@types/counterpart": "^0.18.1",
"@types/flux": "^3.1.9",
"@types/jest": "^26.0.20",
"@types/linkifyjs": "^2.1.3",
"@types/lodash": "^4.14.158",
"@types/lodash": "^4.14.168",
"@types/modernizr": "^3.5.3",
"@types/node": "^12.12.51",
"@types/node": "^14.14.22",
"@types/pako": "^1.0.1",
"@types/qrcode": "^1.3.4",
"@types/qrcode": "^1.3.5",
"@types/react": "^16.9",
"@types/react-dom": "^16.9.8",
"@types/react-dom": "^16.9.10",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^1.23.3",
"@types/sanitize-html": "^1.27.0",
"@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^24.9.0",
"chokidar": "^3.4.1",
"concurrently": "^4.1.2",
"babel-jest": "^26.6.3",
"chokidar": "^3.5.1",
"concurrently": "^5.3.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"eslint": "7.5.0",
"eslint-config-matrix-org": "^0.1.2",
"enzyme-adapter-react-16": "^1.15.6",
"eslint": "7.18.0",
"eslint-config-matrix-org": "^0.2.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-flowtype": "^2.50.3",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^2.5.1",
"glob": "^5.0.15",
"jest": "^26.5.2",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"glob": "^7.1.6",
"jest": "^26.6.3",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom-sixteen": "^1.0.3",
"lolex": "^5.1.2",
"matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"react-test-renderer": "^16.13.1",
"rimraf": "^2.7.1",
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.3.0",
"react-test-renderer": "^16.14.0",
"rimraf": "^3.0.2",
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
"typescript": "^3.9.7",
"typescript": "^4.1.3",
"walk": "^2.3.14"
},
"resolutions": {
"**/@types/react": "^16.14"
},
"jest": {
"testEnvironment": "./__test-utils__/environment.js",
"testMatch": [

View File

@ -180,6 +180,11 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
.mx_LeftPanel_roomListContainer {
width: 68px;
.mx_LeftPanel_userHeader {
flex-direction: row;
justify-content: center;
}
.mx_LeftPanel_filterContainer {
// Organize the flexbox into a centered column layout
flex-direction: column;

View File

@ -219,7 +219,7 @@ hr.mx_RoomView_myReadMarker {
position: relative;
top: -1px;
z-index: 1;
transition: width 400ms easeInSine 1s, opacity 400ms easeInSine 1s;
transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
width: 99%;
opacity: 1;
}

View File

@ -119,14 +119,10 @@ limitations under the License.
}
&.mx_UserMenu_minimized {
.mx_UserMenu_userHeader {
.mx_UserMenu_row {
justify-content: center;
}
padding-right: 0px;
.mx_UserMenu_userAvatarContainer {
margin-right: 0;
}
.mx_UserMenu_userAvatarContainer {
margin-right: 0px;
}
}
}

View File

@ -34,7 +34,7 @@ limitations under the License.
h3 {
font-size: $font-14px;
font-weight: 600;
color: $authpage-primary-color;
color: $authpage-secondary-color;
}
h3.mx_AuthBody_centered {

View File

@ -18,7 +18,7 @@ limitations under the License.
display: flex;
flex-direction: column;
width: 206px;
padding: 25px 40px;
padding: 25px 25px;
box-sizing: border-box;
}

View File

@ -17,7 +17,7 @@ limitations under the License.
.mx_AuthHeaderLogo {
margin-top: 15px;
flex: 1;
padding: 0 10px;
padding: 0 25px;
}
.mx_AuthHeaderLogo img {

View File

@ -83,7 +83,10 @@ limitations under the License.
}
.mx_InteractiveAuthEntryComponents_termsPolicy {
display: block;
display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
}
.mx_InteractiveAuthEntryComponents_passwordSection {

View File

@ -23,6 +23,7 @@ limitations under the License.
font-size: $font-14px;
font-weight: 600;
color: $authpage-lang-color;
width: auto;
}
.mx_AuthBody_language .mx_Dropdown_arrow {

View File

@ -18,7 +18,6 @@ limitations under the License.
display: flex;
flex-direction: column;
align-items: center;
&.mx_WelcomePage_registrationDisabled {
.mx_ButtonCreateAccount {
display: none;
@ -27,6 +26,6 @@ limitations under the License.
}
.mx_Welcome .mx_AuthBody_language {
width: 120px;
width: 160px;
margin-bottom: 10px;
}

View File

@ -89,24 +89,18 @@ limitations under the License.
}
}
.mx_showMore {
display: block;
text-align: left;
margin-top: 10px;
}
.metadata {
color: $muted-fg-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0;
}
.metadata.visible {
overflow-y: visible;
text-overflow: ellipsis;
white-space: normal;
padding: 0;
> li {
padding: 0;
border: 0;
}
}
}
}

View File

@ -16,13 +16,26 @@ limitations under the License.
.mx_SSOButtons {
display: flex;
flex-wrap: wrap;
justify-content: center;
.mx_SSOButtons_row {
& + .mx_SSOButtons_row {
margin-top: 16px;
}
}
.mx_SSOButton {
position: relative;
width: 100%;
padding-left: 32px;
padding-right: 32px;
padding: 7px 32px;
text-align: center;
border-radius: 8px;
display: inline-block;
font-size: $font-14px;
font-weight: $font-semi-bold;
border: 1px solid $input-border-color;
color: $primary-fg-color;
> img {
object-fit: contain;
@ -32,10 +45,22 @@ limitations under the License.
}
}
.mx_SSOButton_default {
color: $button-primary-bg-color;
background-color: $button-secondary-bg-color;
border-color: $button-primary-bg-color;
}
.mx_SSOButton_default.mx_SSOButton_primary {
color: $button-primary-fg-color;
background-color: $button-primary-bg-color;
}
.mx_SSOButton_mini {
box-sizing: border-box;
width: 50px; // 48px + 1px border on all sides
height: 50px; // 48px + 1px border on all sides
min-width: 50px; // prevent crushing by the flexbox
padding: 12px;
> img {
left: 12px;
@ -43,7 +68,7 @@ limitations under the License.
}
& + .mx_SSOButton_mini {
margin-left: 24px;
margin-left: 16px;
}
}
}

View File

@ -59,7 +59,7 @@ limitations under the License.
}
.mx_ServerPicker_server {
color: $primary-fg-color;
color: $authpage-primary-color;
grid-column: 1;
grid-row: 2;
margin-bottom: 16px;

View File

@ -24,26 +24,45 @@ $MiniAppTileHeight: 200px;
flex-direction: column;
overflow: hidden;
.mx_AppsContainer_resizerHandleContainer {
width: 100%;
height: 10px;
margin-top: -3px; // move it up so the interactions are slightly more comfortable
display: block;
position: relative;
}
.mx_AppsContainer_resizerHandle {
cursor: ns-resize;
border-radius: 3px;
// Override styles from library
width: unset !important;
height: 4px !important;
// Override styles from library, making the whole area the target area
width: 100% !important;
height: 100% !important;
// This is positioned directly below frame
position: absolute;
bottom: -8px !important; // override from library
bottom: 0 !important; // override from library
// Together, these make the bar 64px wide
// These are also overridden from the library
left: calc(50% - 32px) !important;
right: calc(50% - 32px) !important;
// We then render the pill handle in an ::after to keep it in the handle's
// area without being a massive line across the screen
&::after {
content: '';
position: absolute;
border-radius: 3px;
// The combination of these two should make the pill 4px high
top: 6px;
bottom: 0;
// Together, these make the bar 64px wide
// These are also overridden from the library
left: calc(50% - 32px);
right: calc(50% - 32px);
}
}
&:hover {
.mx_AppsContainer_resizerHandle {
.mx_AppsContainer_resizerHandle::after {
opacity: 0.8;
background: $primary-fg-color;
}

View File

@ -74,7 +74,6 @@ $left-gutter: 64px;
margin-left: 5px;
display: inline-block;
vertical-align: top;
height: 16px;
overflow: hidden;
user-select: none;

View File

@ -20,7 +20,7 @@ $left-gutter: 64px;
.mx_GroupLayout {
.mx_EventTile {
> .mx_SenderProfile {
line-height: $font-17px;
line-height: $font-20px;
padding-left: $left-gutter;
}
@ -38,7 +38,7 @@ $left-gutter: 64px;
}
.mx_EventTile_line, .mx_EventTile_reply {
padding-top: 3px;
padding-top: 1px;
padding-bottom: 3px;
line-height: $font-22px;
}

View File

@ -207,6 +207,17 @@ $irc-line-height: $font-18px;
width: unset;
max-width: var(--name-width);
}
.mx_SenderProfile_hover {
background: transparent;
> span {
> .mx_SenderProfile_name,
> .mx_SenderProfile_aux {
min-width: inherit;
}
}
}
}
.mx_ProfileResizer {

View File

@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ProfileSettings_controls_topic {
& > textarea {
resize: vertical;
}
}
.mx_ProfileSettings_profile {
display: flex;
}

View File

@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9803 1.2796C17.0771 2.54383 16.6773 3.79601 15.8657 4.77022C15.0784 5.74949 13.8854 6.31354 12.629 6.3006C12.5491 5.07276 12.9605 3.86352 13.7727 2.93921C14.5952 2.00238 15.7404 1.40982 16.9803 1.2796ZM20.9539 8.70795C19.5086 9.59652 18.6192 11.1635 18.5974 12.86C18.5994 14.7794 19.7489 16.5115 21.5166 17.2592C21.1766 18.3636 20.6642 19.4073 19.9982 20.3517C19.1038 21.6896 18.1661 22.9967 16.6777 23.0208C15.9698 23.0372 15.492 22.8336 14.9941 22.6215C14.4747 22.4003 13.9335 22.1697 13.0867 22.1697C12.1885 22.1697 11.6231 22.4077 11.0778 22.6372C10.6065 22.8355 10.1503 23.0275 9.50727 23.0542C8.08982 23.1067 7.00654 21.6263 6.07964 20.3009C4.22703 17.5943 2.78444 12.6733 4.71844 9.32483C5.62662 7.69286 7.32468 6.65727 9.19136 6.59696C9.99528 6.58042 10.7667 6.89028 11.443 7.16193C11.9602 7.36969 12.4219 7.5551 12.7999 7.5551C13.1321 7.5551 13.5809 7.37701 14.1038 7.16946C14.9276 6.84251 15.9356 6.44246 16.9628 6.55027C18.5589 6.60021 20.038 7.39984 20.9539 8.70795Z" fill="#17191C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,9 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="2" y="1" width="22" height="22">
<path d="M2.10154 1.5H23.1003V22.3716H2.10154V1.5Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.1 11.999C23.1 6.20003 18.399 1.49902 12.6 1.49902C6.801 1.49902 2.1 6.20003 2.1 11.999C2.1 17.2399 5.9397 21.5838 10.9594 22.3715V15.0342H8.29336V11.999H10.9594V9.68574C10.9594 7.05418 12.5269 5.60059 14.9254 5.60059C16.0742 5.60059 17.2758 5.80566 17.2758 5.80566V8.38965H15.9518C14.6474 8.38965 14.2406 9.19903 14.2406 10.0294V11.999H17.1527L16.6872 15.0342H14.2406V22.3715C19.2603 21.5838 23.1 17.2399 23.1 11.999Z" fill="#1877F2"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6872 15.0342L17.1527 11.999H14.2406V10.0294C14.2406 9.19903 14.6474 8.38965 15.9518 8.38965H17.2758V5.80566C17.2758 5.80566 16.0742 5.60059 14.9254 5.60059C12.5269 5.60059 10.9594 7.05418 10.9594 9.68574V11.999H8.29336V15.0342H10.9594V22.3715C11.494 22.4553 12.0419 22.499 12.6 22.499C13.1581 22.499 13.706 22.4553 14.2406 22.3715V15.0342H16.6872Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.8421 7.10595C19.9703 5.6121 18.7876 4.42942 17.2939 3.55764C15.8 2.68581 14.169 2.25001 12.3999 2.25001C10.6311 2.25001 8.9996 2.68594 7.50597 3.55764C6.01212 4.42938 4.82953 5.6121 3.95765 7.10595C3.08592 8.59976 2.65002 10.231 2.65002 11.9997C2.65002 14.1242 3.26987 16.0346 4.50987 17.7315C5.74973 19.4284 7.35145 20.6027 9.3149 21.2543C9.54345 21.2967 9.71264 21.2669 9.82265 21.1656C9.9327 21.0641 9.98766 20.937 9.98766 20.7848C9.98766 20.7595 9.98548 20.531 9.98126 20.0993C9.9769 19.6676 9.97485 19.291 9.97485 18.9696L9.68285 19.0202C9.49667 19.0543 9.26181 19.0687 8.97826 19.0646C8.69484 19.0607 8.40061 19.031 8.09598 18.9757C7.79121 18.921 7.50775 18.7941 7.24536 18.5952C6.98311 18.3963 6.79693 18.1359 6.68688 17.8145L6.55993 17.5224C6.47531 17.3279 6.3421 17.1119 6.1601 16.875C5.97811 16.638 5.79406 16.4773 5.60789 16.3927L5.519 16.329C5.45978 16.2868 5.40482 16.2358 5.35399 16.1766C5.30321 16.1174 5.2652 16.0582 5.23981 15.9988C5.21437 15.9395 5.23545 15.8908 5.30326 15.8526C5.37107 15.8144 5.49361 15.7959 5.67143 15.7959L5.92524 15.8338C6.09451 15.8677 6.3039 15.9691 6.55366 16.1384C6.80329 16.3077 7.0085 16.5277 7.16933 16.7984C7.36408 17.1455 7.59873 17.4099 7.87392 17.5919C8.14889 17.7739 8.42613 17.8648 8.70537 17.8648C8.98461 17.8648 9.22579 17.8436 9.429 17.8015C9.63198 17.7592 9.82243 17.6955 10.0002 17.611C10.0764 17.0437 10.2838 16.6079 10.6222 16.3033C10.1399 16.2526 9.70619 16.1762 9.32099 16.0747C8.93601 15.9731 8.53818 15.8081 8.12777 15.5794C7.71714 15.3509 7.37649 15.0673 7.10574 14.7289C6.83495 14.3904 6.61271 13.9459 6.43934 13.3959C6.26588 12.8457 6.17913 12.211 6.17913 11.4916C6.17913 10.4674 6.51351 9.59578 7.18213 8.87633C6.86892 8.10629 6.89849 7.24304 7.27093 6.28668C7.51638 6.21043 7.88037 6.26765 8.36273 6.45801C8.84517 6.64845 9.1984 6.8116 9.42277 6.94686C9.64714 7.08208 9.82692 7.19666 9.96236 7.2896C10.7496 7.06963 11.562 6.95962 12.3998 6.95962C13.2377 6.95962 14.0503 7.06963 14.8376 7.2896L15.32 6.98505C15.6498 6.78185 16.0394 6.59563 16.4877 6.42635C16.9363 6.25716 17.2793 6.21056 17.5164 6.28682C17.8971 7.24322 17.931 8.10642 17.6177 8.87647C18.2863 9.59591 18.6208 10.4677 18.6208 11.4918C18.6208 12.2111 18.5337 12.8478 18.3605 13.4023C18.1871 13.9568 17.963 14.4008 17.688 14.7353C17.4127 15.0697 17.0699 15.3511 16.6595 15.5795C16.249 15.808 15.851 15.973 15.466 16.0747C15.0809 16.1763 14.6472 16.2527 14.1648 16.3035C14.6048 16.6842 14.8248 17.2851 14.8248 18.106V20.7845C14.8248 20.9367 14.8777 21.0637 14.9836 21.1652C15.0894 21.2665 15.2565 21.2964 15.485 21.2539C17.4487 20.6024 19.0505 19.4281 20.2903 17.7311C21.53 16.0343 22.15 14.1238 22.15 11.9993C22.1496 10.2309 21.7135 8.59976 20.8421 7.10595Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0005 20.3296L15.3166 10.1293H8.68921L12.0005 20.3296Z" fill="#E24329"/>
<path d="M4.04348 10.1293L3.03364 13.2283C2.94226 13.5097 3.04095 13.8203 3.28214 13.9957L11.9996 20.3296L4.04348 10.1293Z" fill="#FCA326"/>
<path d="M4.04248 10.1289H8.68727L6.68828 3.98572C6.58597 3.67143 6.1401 3.67143 6.03411 3.98572L4.04248 10.1289Z" fill="#E24329"/>
<path d="M19.9602 10.1293L20.9664 13.2283C21.0577 13.5097 20.9591 13.8203 20.7179 13.9957L11.9991 20.3296L19.9602 10.1293Z" fill="#FCA326"/>
<path d="M19.9616 10.1289H15.3168L17.3121 3.98572C17.4144 3.67143 17.8603 3.67143 17.9663 3.98572L19.9616 10.1289Z" fill="#E24329"/>
<path d="M11.9991 20.3296L15.3153 10.1293H19.9601L11.9991 20.3296Z" fill="#FC6D26"/>
<path d="M11.9985 20.3296L4.04248 10.1293H8.68727L11.9985 20.3296Z" fill="#FC6D26"/>
</svg>

After

Width:  |  Height:  |  Size: 905 B

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.501 12.2333C22.501 11.37 22.4296 10.74 22.2748 10.0867H12.2153V13.9833H18.12C18.001 14.9517 17.3582 16.41 15.9296 17.3899L15.9096 17.5204L19.0902 19.9351L19.3106 19.9567C21.3343 18.125 22.501 15.43 22.501 12.2333Z" fill="#4285F4"/>
<path d="M12.2147 22.5001C15.1075 22.5001 17.5361 21.5667 19.3099 19.9567L15.929 17.39C15.0242 18.0083 13.8099 18.44 12.2147 18.44C9.38142 18.44 6.97669 16.6083 6.11947 14.0767L5.99382 14.0871L2.68656 16.5955L2.64331 16.7133C4.40519 20.1433 8.02423 22.5001 12.2147 22.5001Z" fill="#34A853"/>
<path d="M6.12022 14.0767C5.89403 13.4234 5.76313 12.7233 5.76313 12C5.76313 11.2767 5.89403 10.5767 6.10832 9.92337L6.10233 9.78423L2.75361 7.2356L2.64405 7.28667C1.91789 8.71002 1.50122 10.3084 1.50122 12C1.50122 13.6917 1.91789 15.29 2.64405 16.7133L6.12022 14.0767Z" fill="#FBBC05"/>
<path d="M12.2148 5.55997C14.2267 5.55997 15.5838 6.41163 16.3576 7.12335L19.3814 4.23C17.5243 2.53834 15.1076 1.5 12.2148 1.5C8.02426 1.5 4.4052 3.85665 2.64331 7.28662L6.10759 9.92332C6.97671 7.39166 9.38146 5.55997 12.2148 5.55997Z" fill="#EB4335"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.04155 21C6.6153 21 4.35363 20.2943 2.45 19.0767C4.06624 19.1813 6.91855 18.9308 8.69268 17.2386C6.0238 17.1161 4.82019 15.0692 4.6632 14.1945C4.88997 14.2819 5.97147 14.3869 6.582 14.142C3.51192 13.3722 3.04094 10.678 3.1456 9.85573C3.72124 10.2581 4.69809 10.3981 5.08185 10.3631C2.22109 8.31618 3.25027 5.23707 3.75613 4.57226C5.80911 7.4165 8.8859 9.01393 12.6923 9.10278C12.6205 8.78802 12.5826 8.46032 12.5826 8.12373C12.5826 5.70819 14.5351 3.75 16.9435 3.75C18.2019 3.75 19.3358 4.28457 20.1318 5.13963C20.9727 4.94258 22.2382 4.4813 22.8569 4.0824C22.5451 5.20208 21.5742 6.13612 20.9869 6.48231C20.9918 6.49408 20.9821 6.47048 20.9869 6.48231C21.5028 6.40428 22.8986 6.13603 23.45 5.76192C23.1773 6.39094 22.148 7.4368 21.3033 8.02232C21.4604 14.9535 16.1574 21 9.04155 21Z" fill="#1D9BF0"/>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View File

@ -1,8 +1,7 @@
# Update on docker hub with the following commands in the directory of this file:
# docker build -t vectorim/element-web-ci-e2etests-env:latest .
# docker log
# docker push vectorim/element-web-ci-e2etests-env:latest
FROM node:10
FROM node:14-buster
RUN apt-get update
RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
# dependencies for chrome (installed by puppeteer)

View File

@ -1,29 +1,30 @@
#!/usr/bin/env node
var fs = require('fs');
var path = require('path');
var glob = require('glob');
var args = require('minimist')(process.argv);
var chokidar = require('chokidar');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const util = require('util');
const args = require('minimist')(process.argv);
const chokidar = require('chokidar');
var componentIndex = path.join('src', 'component-index.js');
var componentIndexTmp = componentIndex+".tmp";
var componentsDir = path.join('src', 'components');
var componentJsGlob = '**/*.js';
var componentTsGlob = '**/*.tsx';
var prevFiles = [];
const componentIndex = path.join('src', 'component-index.js');
const componentIndexTmp = componentIndex+".tmp";
const componentsDir = path.join('src', 'components');
const componentJsGlob = '**/*.js';
const componentTsGlob = '**/*.tsx';
let prevFiles = [];
function reskindex() {
var jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort();
var tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort();
var files = [...tsFiles, ...jsFiles];
async function reskindex() {
const jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort();
const tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort();
const files = [...tsFiles, ...jsFiles];
if (!filesHaveChanged(files, prevFiles)) {
return;
}
prevFiles = files;
var header = args.h || args.header;
const header = args.h || args.header;
var strm = fs.createWriteStream(componentIndexTmp);
const strm = fs.createWriteStream(componentIndexTmp);
if (header) {
strm.write(fs.readFileSync(header));
@ -38,11 +39,11 @@ function reskindex() {
strm.write(" */\n\n");
strm.write("let components = {};\n");
for (var i = 0; i < files.length; ++i) {
var file = files[i].replace('.js', '').replace('.tsx', '');
for (let i = 0; i < files.length; ++i) {
const file = files[i].replace('.js', '').replace('.tsx', '');
var moduleName = (file.replace(/\//g, '.'));
var importName = moduleName.replace(/\./g, "$");
const moduleName = (file.replace(/\//g, '.'));
const importName = moduleName.replace(/\./g, "$");
strm.write("import " + importName + " from './components/" + file + "';\n");
strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
@ -51,9 +52,10 @@ function reskindex() {
}
strm.write("export {components};\n");
strm.end();
// Ensure the file has been fully written to disk before proceeding
await util.promisify(strm.end);
fs.rename(componentIndexTmp, componentIndex, function(err) {
if(err) {
if (err) {
console.error("Error moving new index into place: " + err);
} else {
console.log('Reskindex: completed');
@ -67,7 +69,7 @@ function filesHaveChanged(files, prevFiles) {
return true;
}
// Check for name changes
for (var i = 0; i < files.length; i++) {
for (let i = 0; i < files.length; i++) {
if (prevFiles[i] !== files[i]) {
return true;
}
@ -81,7 +83,7 @@ if (!args.w) {
return;
}
var watchDebouncer = null;
let watchDebouncer = null;
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
if (path === componentIndex) return;
if (watchDebouncer) clearTimeout(watchDebouncer);

View File

@ -36,6 +36,7 @@ import {Analytics} from "../Analytics";
import CountlyAnalytics from "../CountlyAnalytics";
import UserActivity from "../UserActivity";
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
declare global {
interface Window {
@ -59,6 +60,7 @@ declare global {
mxNotifier: typeof Notifier;
mxRightPanelStore: RightPanelStore;
mxWidgetStore: WidgetStore;
mxWidgetLayoutStore: WidgetLayoutStore;
mxCallHandler: CallHandler;
mxAnalytics: Analytics;
mxCountlyAnalytics: typeof CountlyAnalytics;

View File

@ -30,6 +30,7 @@ import {idbLoad, idbSave, idbDelete} from "./utils/StorageManager";
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
export const SSO_IDP_ID_KEY = "mx_sso_idp_id";
export enum UpdateCheckStatus {
Checking = "CHECKING",
@ -56,7 +57,7 @@ export default abstract class BasePlatform {
this.startUpdateCheck = this.startUpdateCheck.bind(this);
}
abstract async getConfig(): Promise<{}>;
abstract getConfig(): Promise<{}>;
abstract getDefaultDeviceDisplayName(): string;
@ -258,6 +259,9 @@ export default abstract class BasePlatform {
if (mxClient.getIdentityServerUrl()) {
localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
}
if (idpId) {
localStorage.setItem(SSO_IDP_ID_KEY, idpId);
}
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO
}

View File

@ -83,6 +83,8 @@ import {UIFeature} from "./settings/UIFeature";
import { CallError } from "matrix-js-sdk/src/webrtc/call";
import { logger } from 'matrix-js-sdk/src/logger';
import { Action } from './dispatcher/actions';
import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
const CHECK_PSTN_SUPPORT_ATTEMPTS = 3;
@ -133,6 +135,15 @@ export default class CallHandler {
return window.mxCallHandler;
}
/*
* Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
* if a voip_mxid_translate_pattern is set in the config)
*/
public static roomIdForCall(call: MatrixCall) {
if (!call) return null;
return roomForVirtualRoom(call.roomId) || call.roomId;
}
start() {
this.dispatcherRef = dis.register(this.onAction);
// add empty handlers for media actions, otherwise the media keys
@ -284,11 +295,15 @@ export default class CallHandler {
// We don't allow placing more than one call per room, but that doesn't mean there
// can't be more than one, eg. in a glare situation. This checks that the given call
// is the call we consider 'the' call for its room.
const callForThisRoom = this.getCallForRoom(call.roomId);
const mappedRoomId = CallHandler.roomIdForCall(call);
const callForThisRoom = this.getCallForRoom(mappedRoomId);
return callForThisRoom && call.callId === callForThisRoom.callId;
}
private setCallListeners(call: MatrixCall) {
const mappedRoomId = CallHandler.roomIdForCall(call);
call.on(CallEvent.Error, (err: CallError) => {
if (!this.matchesCallForThisRoom(call)) return;
@ -318,7 +333,7 @@ export default class CallHandler {
Analytics.trackEvent('voip', 'callHangup');
this.removeCallForRoom(call.roomId);
this.removeCallForRoom(mappedRoomId);
});
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
if (!this.matchesCallForThisRoom(call)) return;
@ -342,8 +357,9 @@ export default class CallHandler {
this.play(AudioID.Ringback);
break;
case CallState.Ended:
{
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
this.removeCallForRoom(call.roomId);
this.removeCallForRoom(mappedRoomId);
if (oldState === CallState.InviteSent && (
call.hangupParty === CallParty.Remote ||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
@ -375,10 +391,14 @@ export default class CallHandler {
title: _t("Answered Elsewhere"),
description: _t("The call was answered on another device."),
});
} else if (oldState !== CallState.Fledgling) {
} else if (oldState !== CallState.Fledgling && oldState !== CallState.Ringing) {
// don't play the end-call sound for calls that never got off the ground
this.play(AudioID.CallEnd);
}
this.logCallStats(call, mappedRoomId);
break;
}
}
});
call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
@ -392,25 +412,63 @@ export default class CallHandler {
this.pause(AudioID.Ringback);
}
this.calls.set(newCall.roomId, newCall);
this.calls.set(mappedRoomId, newCall);
this.setCallListeners(newCall);
this.setCallState(newCall, newCall.state);
});
}
private async logCallStats(call: MatrixCall, mappedRoomId: string) {
const stats = await call.getCurrentCallStats();
logger.debug(
`Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` +
`user-facing room ID: ${mappedRoomId}, direction: ${call.direction}, ` +
`our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` +
`hangup reason: ${call.hangupReason}`,
);
logger.debug("Local candidates:");
for (const cand of stats.filter(item => item.type === 'local-candidate')) {
const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
logger.debug(
`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
`protocol: ${cand.protocol}, relay protocol: ${cand.relayProtocol}, network type: ${cand.networkType}`,
);
}
logger.debug("Remote candidates:");
for (const cand of stats.filter(item => item.type === 'remote-candidate')) {
const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
logger.debug(
`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
`protocol: ${cand.protocol}`,
);
}
logger.debug("Candidate pairs:");
for (const pair of stats.filter(item => item.type === 'candidate-pair')) {
logger.debug(
`${pair.localCandidateId} / ${pair.remoteCandidateId} - state: ${pair.state}, ` +
`nominated: ${pair.nominated}, ` +
`requests sent ${pair.requestsSent}, requests received ${pair.requestsReceived}, ` +
`responses received: ${pair.responsesReceived}, responses sent: ${pair.responsesSent}, ` +
`bytes received: ${pair.bytesReceived}, bytes sent: ${pair.bytesSent}, `,
);
}
}
private setCallAudioElement(call: MatrixCall) {
const audioElement = getRemoteAudioElement();
if (audioElement) call.setRemoteAudioElement(audioElement);
}
private setCallState(call: MatrixCall, status: CallState) {
const mappedRoomId = CallHandler.roomIdForCall(call);
console.log(
`Call state in ${call.roomId} changed to ${status}`,
`Call state in ${mappedRoomId} changed to ${status}`,
);
dis.dispatch({
action: 'call_state',
room_id: call.roomId,
room_id: mappedRoomId,
state: status,
});
}
@ -477,14 +535,20 @@ export default class CallHandler {
}, null, true);
}
private placeCall(
private async placeCall(
roomId: string, type: PlaceCallType,
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
) {
Analytics.trackEvent('voip', 'placeCall', 'type', type);
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || roomId;
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
this.calls.set(roomId, call);
this.setCallListeners(call);
this.setCallAudioElement(call);
@ -518,6 +582,12 @@ export default class CallHandler {
switch (payload.action) {
case 'place_call':
{
// We might be using managed hybrid widgets
if (isManagedHybridWidgetEnabled()) {
addManagedHybridWidget(payload.room_id);
return;
}
// if the runtime env doesn't do VoIP, whine.
if (!MatrixClientPeg.get().supportsVoip()) {
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
@ -586,13 +656,14 @@ export default class CallHandler {
const call = payload.call as MatrixCall;
if (this.getCallForRoom(call.roomId)) {
const mappedRoomId = CallHandler.roomIdForCall(call);
if (this.getCallForRoom(mappedRoomId)) {
// ignore multiple incoming calls to the same room
return;
}
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
this.calls.set(call.roomId, call)
this.calls.set(mappedRoomId, call)
this.setCallListeners(call);
}
break;

View File

@ -497,7 +497,7 @@ export default class ContentMessages {
content.info.mimetype = file.type;
}
const prom = new Promise((resolve) => {
const prom = new Promise<void>((resolve) => {
if (file.type.indexOf('image/') === 0) {
content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {

View File

@ -840,7 +840,7 @@ export default class CountlyAnalytics {
let endTime = CountlyAnalytics.getTimestamp();
const cli = MatrixClientPeg.get();
if (!cli.getRoom(roomId)) {
await new Promise(resolve => {
await new Promise<void>(resolve => {
const handler = (room) => {
if (room.roomId === roomId) {
cli.off("Room", handler);
@ -880,7 +880,7 @@ export default class CountlyAnalytics {
let endTime = CountlyAnalytics.getTimestamp();
if (!room.findEventById(eventId)) {
await new Promise(resolve => {
await new Promise<void>(resolve => {
const handler = (ev) => {
if (ev.getId() === eventId) {
room.off("Room.localEchoUpdated", handler);

View File

@ -422,6 +422,8 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
if (SettingsStore.getValue("feature_latex_maths")) {
const phtml = cheerio.load(safeBody,
{ _useHtmlParser2: true, decodeEntities: false })
// @ts-ignore - The types for `replaceWith` wrongly expect
// Cheerio instance to be returned.
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
return katex.renderToString(
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),

View File

@ -165,6 +165,7 @@ export default class IdentityAuthClient {
});
const [confirmed] = await finished;
if (confirmed) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useDefaultIdentityServer();
} else {
throw new AbortedIdentityActionError(

View File

@ -46,11 +46,13 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
import {Mjolnir} from "./mjolnir/Mjolnir";
import DeviceListener from "./DeviceListener";
import {Jitsi} from "./widgets/Jitsi";
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY} from "./BasePlatform";
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
import CountlyAnalytics from "./CountlyAnalytics";
import CallHandler from './CallHandler';
import LifecycleCustomisations from "./customisations/Lifecycle";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import {_t} from "./languageHandler";
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";
@ -162,7 +164,8 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
* query-parameters extracted from the real query-string of the starting
* URI.
*
* @param {String} defaultDeviceDisplayName
* @param {string} defaultDeviceDisplayName
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
*
* @returns {Promise} promise which resolves to true if we completed the token
* login, else false
@ -170,6 +173,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
export function attemptTokenLogin(
queryParams: Record<string, string>,
defaultDeviceDisplayName?: string,
fragmentAfterLogin?: string,
): Promise<boolean> {
if (!queryParams.loginToken) {
return Promise.resolve(false);
@ -179,6 +183,12 @@ export function attemptTokenLogin(
const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY);
if (!homeserver) {
console.warn("Cannot log in with token: can't determine HS URL to use");
Modal.createTrackedDialog("SSO", "Unknown HS", ErrorDialog, {
title: _t("We couldn't log you in"),
description: _t("We asked the browser to remember which homeserver you use to let you sign in, " +
"but unfortunately your browser has forgotten it. Go to the sign in page and try again."),
button: _t("Try again"),
});
return Promise.resolve(false);
}
@ -198,8 +208,28 @@ export function attemptTokenLogin(
return true;
});
}).catch((err) => {
console.error("Failed to log in with login token: " + err + " " +
err.data);
Modal.createTrackedDialog("SSO", "Token Rejected", ErrorDialog, {
title: _t("We couldn't log you in"),
description: err.name === "ConnectionError"
? _t("Your homeserver was unreachable and was not able to log you in. Please try again. " +
"If this continues, please contact your homeserver administrator.")
: _t("Your homeserver rejected your log in attempt. " +
"This could be due to things just taking too long. Please try again. " +
"If this continues, please contact your homeserver administrator."),
button: _t("Try again"),
onFinished: tryAgain => {
if (tryAgain) {
const cli = Matrix.createClient({
baseUrl: homeserver,
idBaseUrl: identityServer,
});
const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined;
PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId);
}
},
});
console.error("Failed to log in with login token:");
console.error(err);
return false;
});
}
@ -366,7 +396,7 @@ async function abortLogin() {
// The plan is to gradually move the localStorage access done here into
// SessionStore to avoid bugs where the view becomes out-of-sync with
// localStorage (e.g. isGuest etc.)
async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
const ignoreGuest = opts?.ignoreGuest;
if (!localStorage) {

View File

@ -33,10 +33,20 @@ interface IPasswordFlow {
type: "m.login.password";
}
export enum IdentityProviderBrand {
Gitlab = "org.matrix.gitlab",
Github = "org.matrix.github",
Apple = "org.matrix.apple",
Google = "org.matrix.google",
Facebook = "org.matrix.facebook",
Twitter = "org.matrix.twitter",
}
export interface IIdentityProvider {
id: string;
name: string;
icon?: string;
brand?: IdentityProviderBrand | string;
}
export interface ISSOFlow {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import commonmark from 'commonmark';
import * as commonmark from 'commonmark';
import {escape} from "lodash";
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];

View File

@ -48,6 +48,7 @@ import SettingsStore from "./settings/SettingsStore";
import {UIFeature} from "./settings/UIFeature";
import {CHAT_EFFECTS} from "./effects"
import CallHandler from "./CallHandler";
import {guessAndSetDMRoom} from "./Rooms";
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event {
@ -1112,6 +1113,24 @@ export const Commands = [
return success();
},
}),
new Command({
command: "converttodm",
description: _td("Converts the room to a DM"),
category: CommandCategories.other,
runFn: function(roomId, args) {
const room = MatrixClientPeg.get().getRoom(roomId);
return success(guessAndSetDMRoom(room, true));
},
}),
new Command({
command: "converttoroom",
description: _td("Converts the DM to a room"),
category: CommandCategories.other,
runFn: function(roomId, args) {
const room = MatrixClientPeg.get().getRoom(roomId);
return success(guessAndSetDMRoom(room, false));
},
}),
// Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes

View File

@ -19,6 +19,7 @@ import * as Roles from './Roles';
import {isValid3pidInvite} from "./RoomInvite";
import SettingsStore from "./settings/SettingsStore";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages"
@ -477,6 +478,11 @@ function textForWidgetEvent(event) {
}
}
function textForWidgetLayoutEvent(event) {
const senderName = event.sender?.name || event.getSender();
return _t("%(senderName)s has updated the widget layout", {senderName});
}
function textForMjolnirEvent(event) {
const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent();
@ -583,6 +589,7 @@ const stateHandlers = {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
'im.vector.modular.widgets': textForWidgetEvent,
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
};
// Add all the Mjolnir stuff to the renderer

79
src/VoipUserMapper.ts Normal file
View File

@ -0,0 +1,79 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ensureDMExists, findDMForUser } from './createRoom';
import { MatrixClientPeg } from "./MatrixClientPeg";
import DMRoomMap from "./utils/DMRoomMap";
import SdkConfig from "./SdkConfig";
// Functions for mapping users & rooms for the voip_mxid_translate_pattern
// config option
export function voipUserMapperEnabled(): boolean {
return SdkConfig.get()['voip_mxid_translate_pattern'] !== undefined;
}
// only exported for tests
export function userToVirtualUser(userId: string, templateString?: string): string {
if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
if (!templateString) return null;
return templateString.replace('${mxid}', encodeURIComponent(userId).replace(/%/g, '=').toLowerCase());
}
// only exported for tests
export function virtualUserToUser(userId: string, templateString?: string): string {
if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
if (!templateString) return null;
const regexString = templateString.replace('${mxid}', '(.+)');
const match = userId.match('^' + regexString + '$');
if (!match) return null;
return decodeURIComponent(match[1].replace(/=/g, '%'));
}
async function getOrCreateVirtualRoomForUser(userId: string):Promise<string> {
const virtualUser = userToVirtualUser(userId);
if (!virtualUser) return null;
return await ensureDMExists(MatrixClientPeg.get(), virtualUser);
}
export async function getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> {
const user = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (!user) return null;
return getOrCreateVirtualRoomForUser(user);
}
export function roomForVirtualRoom(roomId: string):string {
const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (!virtualUser) return null;
const realUser = virtualUserToUser(virtualUser);
const room = findDMForUser(MatrixClientPeg.get(), realUser);
if (room) {
return room.roomId;
} else {
return null;
}
}
export function isVirtualRoom(roomId: string):boolean {
const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (!virtualUser) return null;
const realUser = virtualUserToUser(virtualUser);
return Boolean(realUser);
}

View File

@ -168,6 +168,12 @@ const shortcuts: Record<Categories, IShortcut[]> = {
key: Key.U,
}],
description: _td("Upload a file"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.F,
}],
description: _td("Search (must be enabled)"),
},
],

View File

@ -95,7 +95,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const blob = new Blob([this._keyBackupInfo.recovery_key], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'recovery-key.txt');
FileSaver.saveAs(blob, 'security-key.txt');
this.setState({
downloaded: true,
@ -238,7 +238,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
)}</p>
<p>{_t(
"We'll store an encrypted copy of your keys on our server. " +
"Secure your backup with a recovery passphrase.",
"Secure your backup with a Security Phrase.",
)}</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p>
@ -252,10 +252,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
autoFocus={true}
label={_td("Enter a recovery passphrase")}
labelEnterPassword={_td("Enter a recovery passphrase")}
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/>
</div>
</div>
@ -270,7 +270,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<details>
<summary>{_t("Advanced")}</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a recovery key")}
{_t("Set up with a Security Key")}
</AccessibleButton>
</details>
</form>;
@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
"Please enter your recovery passphrase a second time to confirm.",
"Please enter your Security Phrase a second time to confirm.",
)}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -319,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your recovery passphrase...")}
placeholder={_t("Repeat your Security Phrase...")}
autoFocus={true}
/>
</div>
@ -338,15 +338,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseShowKey() {
return <div>
<p>{_t(
"Your recovery key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your recovery passphrase.",
"Your Security Key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your Security Phrase.",
)}</p>
<p>{_t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
)}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your recovery key")}
{_t("Your Security Key")}
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey">
@ -369,12 +369,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
let introText;
if (this.state.copied) {
introText = _t(
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:",
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>},
);
} else if (this.state.downloaded) {
introText = _t(
"Your recovery key is in your <b>Downloads</b> folder.",
"Your Security Key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>},
);
}
@ -433,14 +433,14 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_titleForPhase(phase) {
switch (phase) {
case PHASE_PASSPHRASE:
return _t('Secure your backup with a recovery passphrase');
return _t('Secure your backup with a Security Phrase');
case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm your recovery passphrase');
return _t('Confirm your Security Phrase');
case PHASE_OPTOUT_CONFIRM:
return _t('Warning!');
case PHASE_SHOWKEY:
case PHASE_KEEPITSAFE:
return _t('Make a copy of your recovery key');
return _t('Make a copy of your Security Key');
case PHASE_BACKINGUP:
return _t('Starting backup...');
case PHASE_DONE:

View File

@ -235,7 +235,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const blob = new Blob([this._recoveryKey.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'recovery-key.txt');
FileSaver.saveAs(blob, 'security-key.txt');
this.setState({
downloaded: true,
@ -593,10 +593,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
autoFocus={true}
label={_td("Enter a recovery passphrase")}
labelEnterPassword={_td("Enter a recovery passphrase")}
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/>
</div>

View File

@ -58,7 +58,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
</span>;
const newMethodDetected = <p>{_t(
"A new recovery passphrase and key for Secure Messages have been detected.",
"A new Security Phrase and key for Secure Messages have been detected.",
)}</p>;
const hackWarning = <p className="warning">{_t(

View File

@ -56,7 +56,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
>
<div>
<p>{_t(
"This session has detected that your recovery passphrase and key " +
"This session has detected that your Security Phrase and key " +
"for Secure Messages have been removed.",
)}</p>
<p>{_t(

View File

@ -45,7 +45,7 @@ class FilePanel extends React.Component {
};
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
if (room.roomId !== this.props.roomId) return;
if (room?.roomId !== this.props?.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
if (ev.isBeingDecrypted()) {

View File

@ -177,7 +177,14 @@ export default class InteractiveAuthComponent extends React.Component {
stageState: stageState,
errorText: stageState.error,
}, () => {
if (oldStage != stageType) this._setFocus();
if (oldStage !== stageType) {
this._setFocus();
} else if (
!stageState.error && this._stageComponent.current &&
this._stageComponent.current.attemptFailed
) {
this._stageComponent.current.attemptFailed();
}
});
};

View File

@ -56,7 +56,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
useEffect(onResize, [expanded]);
useEffect(onResize, [expanded, onResize]);
const [onFocus, isActive, ref] = useRovingTabIndex();
const tabIndex = isActive ? 0 : -1;

View File

@ -216,10 +216,12 @@ class LoggedInView extends React.Component<IProps, IState> {
_createResizer() {
let size;
let collapsed;
const collapseConfig: ICollapseConfig = {
toggleSize: 260 - 50,
onCollapsed: (collapsed) => {
if (collapsed) {
onCollapsed: (_collapsed) => {
collapsed = _collapsed;
if (_collapsed) {
dis.dispatch({action: "hide_left_panel"}, true);
window.localStorage.setItem("mx_lhs_size", '0');
} else {
@ -234,7 +236,7 @@ class LoggedInView extends React.Component<IProps, IState> {
this.props.resizeNotifier.startResizing();
},
onResizeStop: () => {
window.localStorage.setItem("mx_lhs_size", '' + size);
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
this.props.resizeNotifier.stopResizing();
},
};
@ -426,6 +428,14 @@ class LoggedInView extends React.Component<IProps, IState> {
handled = true;
}
break;
case Key.F:
if (ctrlCmdOnly && SettingsStore.getValue("ctrlFForSearch")) {
dis.dispatch({
action: 'focus_search',
});
handled = true;
}
break;
case Key.BACKTICK:
// Ideally this would be CTRL+P for "Profile", but that's
// taken by the print dialog. CTRL+I for "Information"

View File

@ -81,6 +81,7 @@ import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from
import {UIFeature} from "../../settings/UIFeature";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
/** constants for MatrixChat.state.view */
export enum Views {
@ -218,6 +219,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private screenAfterLogin?: IScreen;
private windowWidth: number;
private pageChanging: boolean;
private tokenLogin?: boolean;
private accountPassword?: string;
private accountPasswordTimer?: NodeJS.Timeout;
private focusComposer: boolean;
@ -323,13 +325,21 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Lifecycle.attemptTokenLogin(
this.props.realQueryParams,
this.props.defaultDeviceDisplayName,
).then((loggedIn) => {
if (loggedIn) {
this.getFragmentAfterLogin(),
).then(async (loggedIn) => {
if (this.props.realQueryParams?.loginToken) {
// remove the loginToken from the URL regardless
this.props.onTokenLoginCompleted();
}
// don't do anything else until the page reloads - just stay in
// the 'loading' state.
return;
if (loggedIn) {
this.tokenLogin = true;
// Create and start the client
await Lifecycle.restoreFromLocalStorage({
ignoreGuest: true,
});
return this.postLoginSetup();
}
// if the user has followed a login or register link, don't reanimate
@ -353,6 +363,42 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
CountlyAnalytics.instance.enable(/* anonymous = */ true);
}
private async postLoginSetup() {
const cli = MatrixClientPeg.get();
const cryptoEnabled = cli.isCryptoEnabled();
if (!cryptoEnabled) {
this.onLoggedIn();
}
const promisesList = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account
promisesList.push(cli.downloadKeys([cli.getUserId()]));
}
// Now update the state to say we're waiting for the first sync to complete rather
// than for the login to finish.
this.setState({ pendingInitialSync: true });
await Promise.all(promisesList);
if (!cryptoEnabled) {
this.setState({ pendingInitialSync: false });
return;
}
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
if (crossSigningIsSetUp) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
} else {
this.onLoggedIn();
}
this.setState({ pendingInitialSync: false });
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
// eslint-disable-next-line camelcase
UNSAFE_componentWillUpdate(props, state) {
@ -1186,6 +1232,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
) {
showAnalyticsToast(this.props.config.piwik?.policyUrl);
}
if (SdkConfig.get().mobileGuideToast) {
// The toast contains further logic to detect mobile platforms,
// check if it has been dismissed before, etc.
showMobileGuideToast();
}
}
private showScreenAfterLogin() {
@ -1833,40 +1884,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// Create and start the client
await Lifecycle.setLoggedIn(credentials);
const cli = MatrixClientPeg.get();
const cryptoEnabled = cli.isCryptoEnabled();
if (!cryptoEnabled) {
this.onLoggedIn();
}
const promisesList = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account
promisesList.push(cli.downloadKeys([cli.getUserId()]));
}
// Now update the state to say we're waiting for the first sync to complete rather
// than for the login to finish.
this.setState({ pendingInitialSync: true });
await Promise.all(promisesList);
if (!cryptoEnabled) {
this.setState({ pendingInitialSync: false });
return;
}
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
if (crossSigningIsSetUp) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
} else {
this.onLoggedIn();
}
this.setState({ pendingInitialSync: false });
await this.postLoginSetup();
};
// complete security / e2e setup has finished
@ -1910,6 +1928,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
<E2eSetup
onFinished={this.onCompleteSecurityE2eSetupFinished}
accountPassword={this.accountPassword}
tokenLogin={!!this.tokenLogin}
/>
);
} else if (this.state.view === Views.LOGGED_IN) {

View File

@ -23,6 +23,7 @@ import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils';
import * as sdk from '../../index';
import dis from "../../dispatcher/dispatcher";
import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
@ -207,11 +208,13 @@ export default class MessagePanel extends React.Component {
componentDidMount() {
this._isMounted = true;
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
dis.unregister(this.dispatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@ -224,6 +227,14 @@ export default class MessagePanel extends React.Component {
}
}
onAction = (payload) => {
switch (payload.action) {
case "message_sent":
this.scrollToBottom();
break;
}
}
onShowTypingNotificationsChange = () => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),

View File

@ -487,7 +487,11 @@ export default class RoomDirectory extends React.Component {
let previewButton;
let joinOrViewButton;
if (room.world_readable && !hasJoinedRoom) {
// Element Web currently does not allow guests to join rooms, so we
// instead show them preview buttons for all rooms. If the room is not
// world readable, a modal will appear asking you to register first. If
// it is readable, the preview appears as normal.
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
previewButton = (
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>{_t("Preview")}</AccessibleButton>
);
@ -496,7 +500,7 @@ export default class RoomDirectory extends React.Component {
joinOrViewButton = (
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>{_t("View")}</AccessibleButton>
);
} else if (!isGuest || room.guest_can_join) {
} else if (!isGuest) {
joinOrViewButton = (
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>{_t("Join")}</AccessibleButton>
);

View File

@ -21,15 +21,15 @@ limitations under the License.
// - Search results component
// - Drag and drop
import React, {createRef} from 'react';
import React, { createRef } from 'react';
import classNames from 'classnames';
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {EventSubscription} from "fbemitter";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventSubscription } from "fbemitter";
import shouldHideEvent from '../../shouldHideEvent';
import {_t} from '../../languageHandler';
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
import { _t } from '../../languageHandler';
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
@ -40,8 +40,8 @@ import Tinter from '../../Tinter';
import rateLimitedFunc from '../../ratelimitedfunc';
import * as ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms';
import eventSearch, {searchPagination} from '../../Searching';
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard';
import eventSearch, { searchPagination } from '../../Searching';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key } from '../../Keyboard';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore';
@ -50,13 +50,13 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import {haveTileForEvent} from "../views/rooms/EventTile";
import { haveTileForEvent } from "../views/rooms/EventTile";
import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions";
import {SettingLevel} from "../../settings/SettingLevel";
import {IMatrixClientCreds} from "../../MatrixClientPeg";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
import { Action } from "../../dispatcher/actions";
import { SettingLevel } from "../../settings/SettingLevel";
import { IMatrixClientCreds } from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary";
@ -67,17 +67,18 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
import {XOR} from "../../@types/common";
import { XOR } from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay";
import {containsEmoji} from '../../effects/utils';
import {CHAT_EFFECTS} from '../../effects';
import { containsEmoji } from '../../effects/utils';
import { CHAT_EFFECTS } from '../../effects';
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import WidgetStore from "../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../stores/AsyncStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import Notifier from "../../Notifier";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -266,12 +267,6 @@ export default class RoomView extends React.Component<IProps, IState> {
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
}
// TODO: [REACT-WARNING] Move into constructor
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this.onRoomViewStoreUpdate(true);
}
private onWidgetStoreUpdate = () => {
if (this.state.room) {
this.checkWidgets(this.state.room);
@ -280,8 +275,9 @@ export default class RoomView extends React.Component<IProps, IState> {
private checkWidgets = (room) => {
this.setState({
hasPinnedWidgets: WidgetStore.instance.getPinnedApps(room.roomId).length > 0,
})
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top).length > 0,
showApps: this.shouldShowApps(room),
});
};
private onReadReceiptsChange = () => {
@ -418,11 +414,17 @@ export default class RoomView extends React.Component<IProps, IState> {
}
private onWidgetEchoStoreUpdate = () => {
if (!this.state.room) return;
this.setState({
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(this.state.room, Container.Top).length > 0,
showApps: this.shouldShowApps(this.state.room),
});
};
private onWidgetLayoutChange = () => {
this.onWidgetEchoStoreUpdate(); // we cheat here by calling the thing that matters
};
private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean) {
// if this is an unknown room then we're in one of three states:
// - This is a room we can peek into (search engine) (we can /peek)
@ -488,7 +490,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}
private shouldShowApps(room: Room) {
if (!BROWSER_SUPPORTS_SANDBOX) return false;
if (!BROWSER_SUPPORTS_SANDBOX || !room) return false;
// Check if user has previously chosen to hide the app drawer for this
// room. If so, do not show apps
@ -497,10 +499,15 @@ export default class RoomView extends React.Component<IProps, IState> {
// This is confusing, but it means to say that we default to the tray being
// hidden unless the user clicked to open it.
return hideWidgetDrawer === "false";
const isManuallyShown = hideWidgetDrawer === "false";
const widgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
return widgets.length > 0 || isManuallyShown;
}
componentDidMount() {
this.onRoomViewStoreUpdate(true);
const call = this.getCallForRoom();
const callState = call ? call.state : null;
this.setState({
@ -608,6 +615,13 @@ export default class RoomView extends React.Component<IProps, IState> {
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
if (this.state.room) {
WidgetLayoutStore.instance.off(
WidgetLayoutStore.emissionForRoom(this.state.room),
this.onWidgetLayoutChange,
);
}
if (this.showReadReceiptsWatchRef) {
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
}
@ -748,6 +762,9 @@ export default class RoomView extends React.Component<IProps, IState> {
});
}
break;
case 'focus_search':
this.onSearchClick();
break;
}
};
@ -835,6 +852,10 @@ export default class RoomView extends React.Component<IProps, IState> {
// called when state.room is first initialised (either at initial load,
// after a successful peek, or after we join the room).
private onRoomLoaded = (room: Room) => {
// Attach a widget store listener only when we get a room
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.onWidgetLayoutChange(); // provoke an update
this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
this.loadMembersIfJoined(room);
@ -897,6 +918,15 @@ export default class RoomView extends React.Component<IProps, IState> {
if (!room || room.roomId !== this.state.roomId) {
return;
}
// Detach the listener if the room is changing for some reason
if (this.state.room) {
WidgetLayoutStore.instance.off(
WidgetLayoutStore.emissionForRoom(this.state.room),
this.onWidgetLayoutChange,
);
}
this.setState({
room: room,
}, () => {

View File

@ -20,7 +20,6 @@ import * as React from "react";
import {_t} from '../../languageHandler';
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar';
import { ReactNode } from "react";
/**
* Represents a tab for the TabbedView.

View File

@ -24,6 +24,7 @@ export default class E2eSetup extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
accountPassword: PropTypes.string,
tokenLogin: PropTypes.bool,
};
render() {
@ -33,6 +34,7 @@ export default class E2eSetup extends React.Component {
<CreateCrossSigningDialog
onFinished={this.props.onFinished}
accountPassword={this.props.accountPassword}
tokenLogin={this.props.tokenLogin}
/>
</CompleteSecurityBody>
</AuthPage>

View File

@ -340,8 +340,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
};
onTryRegisterClick = ev => {
const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
const ssoFlow = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
const hasPasswordFlow = this.state.flows?.find(flow => flow.type === "m.login.password");
const ssoFlow = this.state.flows?.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
// If has no password flow but an SSO flow guess that the user wants to register with SSO.
// TODO: instead hide the Register button if registration is disabled by checking with the server,
// has no specific errCode currently and uses M_FORBIDDEN.

View File

@ -120,9 +120,9 @@ export default class SetupEncryptionBody extends React.Component {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
recoveryKeyPrompt = _t("Use Security Key or Phrase");
} else if (store.keyInfo) {
recoveryKeyPrompt = _t("Use Recovery Key");
recoveryKeyPrompt = _t("Use Security Key");
}
let useRecoveryKeyButton;

View File

@ -609,8 +609,12 @@ export class SSOAuthEntry extends React.Component {
this.props.authSessionId,
);
this._popupWindow = null;
window.addEventListener("message", this._onReceiveMessage);
this.state = {
phase: SSOAuthEntry.PHASE_PREAUTH,
attemptFailed: false,
};
}
@ -618,12 +622,35 @@ export class SSOAuthEntry extends React.Component {
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
}
componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage);
if (this._popupWindow) {
this._popupWindow.close();
this._popupWindow = null;
}
}
attemptFailed = () => {
this.setState({
attemptFailed: true,
});
};
_onReceiveMessage = event => {
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
if (this._popupWindow) {
this._popupWindow.close();
this._popupWindow = null;
}
}
};
onStartAuthClick = () => {
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
// certainly will need to open the thing in a new tab to avoid losing application
// context.
window.open(this._ssoUrl, '_blank');
this._popupWindow = window.open(this._ssoUrl, "_blank");
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
};
@ -656,10 +683,28 @@ export class SSOAuthEntry extends React.Component {
);
}
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
{cancelButton}
{continueButton}
</div>;
let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{ this.props.errorText }
</div>
);
} else if (this.state.attemptFailed) {
errorSection = (
<div className="error" role="alert">
{ _t("Something went wrong in confirming your identity. Cancel and try again.") }
</div>
);
}
return <React.Fragment>
{ errorSection }
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
{cancelButton}
{continueButton}
</div>
</React.Fragment>;
}
}
@ -710,8 +755,7 @@ export class FallbackAuthEntry extends React.Component {
this.props.loginType,
this.props.authSessionId,
);
this._popupWindow = window.open(url);
this._popupWindow.opener = null;
this._popupWindow = window.open(url, "_blank");
};
_onReceiveMessage = event => {

View File

@ -196,7 +196,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
// Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve.
await new Promise(resolve => this.setState({}, resolve));
await new Promise<void>(resolve => this.setState({}, resolve));
if (this.allFieldsValid()) {
return true;

View File

@ -194,7 +194,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
// Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve.
await new Promise(resolve => this.setState({}, resolve));
await new Promise<void>(resolve => this.setState({}, resolve));
if (this.allFieldsValid()) {
return true;

View File

@ -20,7 +20,7 @@ import {MatrixCapabilities} from "matrix-widget-api";
import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu";
import {ChevronFace} from "../../structures/ContextMenu";
import {_t} from "../../../languageHandler";
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
import {IApp} from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
import RoomContext from "../../../contexts/RoomContext";
@ -30,6 +30,7 @@ import Modal from "../../../Modal";
import QuestionDialog from "../dialogs/QuestionDialog";
import {WidgetType} from "../../../widgets/WidgetType";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
app: IApp;
@ -56,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
let unpinButton;
if (showUnpin) {
const onUnpinClick = () => {
WidgetStore.instance.unpinWidget(room.roomId, app.id);
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right);
onFinished();
};
@ -137,13 +138,13 @@ const WidgetContextMenu: React.FC<IProps> = ({
revokeButton = <IconizedContextMenuOption onClick={onRevokeClick} label={_t("Revoke permissions")} />;
}
const pinnedWidgets = WidgetStore.instance.getPinnedApps(roomId);
const pinnedWidgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
const widgetIndex = pinnedWidgets.findIndex(widget => widget.id === app.id);
let moveLeftButton;
if (showUnpin && widgetIndex > 0) {
const onClick = () => {
WidgetStore.instance.movePinnedWidget(roomId, app.id, -1);
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, -1);
onFinished();
};
@ -153,7 +154,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
let moveRightButton;
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
const onClick = () => {
WidgetStore.instance.movePinnedWidget(roomId, app.id, 1);
WidgetLayoutStore.instance.moveWithinContainer(room, Container.Top, app, 1);
onFinished();
};

View File

@ -50,6 +50,10 @@ export default class ErrorDialog extends React.Component {
button: null,
};
onClick = () => {
this.props.onFinished(true);
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
@ -64,7 +68,7 @@ export default class ErrorDialog extends React.Component {
{ this.props.description || _t('An error has occurred.') }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
<button className="mx_Dialog_primary" onClick={this.onClick} autoFocus={this.props.focus}>
{ this.props.button || _t('OK') }
</button>
</div>

View File

@ -35,13 +35,13 @@ import {
} from "matrix-widget-api";
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import RoomViewStore from "../../../stores/RoomViewStore";
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
import { arrayFastClone } from "../../../utils/arrays";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
interface IProps {
widgetDefinition: IModalWidgetOpenRequestData;
widgetRoomId?: string;
sourceWidgetId: string;
onFinished(success: boolean, data?: IModalWidgetReturnData): void;
}
@ -123,7 +123,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
public render() {
const templated = this.widget.getCompleteUrl({
currentRoomId: RoomViewStore.getRoomId(),
widgetRoomId: this.props.widgetRoomId,
currentUserId: MatrixClientPeg.get().getUserId(),
userDisplayName: OwnProfileStore.instance.displayName,
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),

View File

@ -47,7 +47,7 @@ export default class NewSessionReviewDialog extends React.PureComponent {
<li>{_t("The internet connection either session is using")}</li>
</ul>
<div>
{_t("We recommend you change your password and recovery key in Settings immediately")}
{_t("We recommend you change your password and Security Key in Settings immediately")}
</div>
</div>,
onFinished: () => this.props.onFinished(false),

View File

@ -44,7 +44,8 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
const [email, setEmail] = useState("");
const fieldRef = useRef<Field>();
const onSubmit = async () => {
const onSubmit = async (e) => {
e.preventDefault();
if (email) {
const valid = await fieldRef.current.validate({ allowEmpty: false });
@ -73,6 +74,7 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
<form onSubmit={onSubmit}>
<Field
ref={fieldRef}
autoFocus={true}
type="text"
label={_t("Email (optional)")}
value={email}

View File

@ -151,13 +151,13 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
const valid = await this.fieldRef.current.validate({ allowEmpty: false });
if (!valid) {
if (!valid && !this.state.defaultChosen) {
this.fieldRef.current.focus();
this.fieldRef.current.validate({ allowEmpty: false, focused: true });
return;
}
this.props.onFinished(this.validatedConf);
this.props.onFinished(this.state.defaultChosen ? this.defaultServer : this.validatedConf);
};
public render() {

View File

@ -70,26 +70,26 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
return (
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
onFinished={this.props.onFinished}
title={_t("A widget would like to verify your identity")}>
title={_t("Allow this widget to verify your identity")}>
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
<p>
{_t(
"A widget located at %(widgetUrl)s would like to verify your identity. " +
"By allowing this, the widget will be able to verify your user ID, but not " +
"perform actions as you.", {
widgetUrl: this.props.widget.templateUrl.split("?")[0],
},
)}
{_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
</p>
<p className="text-muted">
{/* cheap trim to just get the path */}
{this.props.widget.templateUrl.split("?")[0].split("#")[0]}
</p>
<LabelledToggleSwitch value={this.state.rememberSelection} toggleInFront={true}
onChange={this._onRememberSelectionChange}
label={_t("Remember my selection for this widget")} />
</div>
<DialogButtons
primaryButton={_t("Allow")}
primaryButton={_t("Continue")}
onPrimaryButtonClick={this._onAllow}
cancelButton={_t("Deny")}
onCancel={this._onDeny}
additive={
<LabelledToggleSwitch
value={this.state.rememberSelection}
toggleInFront={true}
onChange={this._onRememberSelectionChange}
label={_t("Remember this")} />}
/>
</BaseDialog>
);

View File

@ -199,11 +199,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else if (this.state.recoveryKeyCorrect) {
return _t("Looks good!");
} else if (this.state.recoveryKeyValid) {
return _t("Wrong Recovery Key");
return _t("Wrong Security Key");
} else if (this.state.recoveryKeyValid === null) {
return '';
} else {
return _t("Invalid Recovery Key");
return _t("Invalid Security Key");
}
}
@ -231,7 +231,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. " +
"Please verify that you entered the correct recovery passphrase.",
"Please verify that you entered the correct Security Phrase.",
)}
</div>;
} else {

View File

@ -34,6 +34,7 @@ import InteractiveAuthDialog from '../InteractiveAuthDialog';
export default class CreateCrossSigningDialog extends React.PureComponent {
static propTypes = {
accountPassword: PropTypes.string,
tokenLogin: PropTypes.bool,
};
constructor(props) {
@ -96,6 +97,9 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
user: MatrixClientPeg.get().getUserId(),
password: this.state.accountPassword,
});
} else if (this.props.tokenLogin) {
// We are hoping the grace period is active
await makeRequest({});
} else {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
@ -144,6 +148,12 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
});
this.props.onFinished(true);
} catch (e) {
if (this.props.tokenLogin) {
// ignore any failures, we are relying on grace period here
this.props.onFinished();
return;
}
this.setState({ error: e });
console.error("Error bootstrapping cross-signing", e);
}

View File

@ -297,19 +297,19 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
} else if (this.state.restoreError) {
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
title = _t("Recovery key mismatch");
title = _t("Security Key mismatch");
content = <div>
<p>{_t(
"Backup could not be decrypted with this recovery key: " +
"please verify that you entered the correct recovery key.",
"Backup could not be decrypted with this Security Key: " +
"please verify that you entered the correct Security Key.",
)}</p>
</div>;
} else {
title = _t("Incorrect recovery passphrase");
title = _t("Incorrect Security Phrase");
content = <div>
<p>{_t(
"Backup could not be decrypted with this recovery passphrase: " +
"please verify that you entered the correct recovery passphrase.",
"Backup could not be decrypted with this Security Phrase: " +
"please verify that you entered the correct Security Phrase.",
)}</p>
</div>;
}
@ -342,7 +342,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter recovery passphrase");
title = _t("Enter Security Phrase");
content = <div>
<p>{_t(
"<b>Warning</b>: you should only set up key backup " +
@ -351,7 +351,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
)}</p>
<p>{_t(
"Access your secure message history and set up secure " +
"messaging by entering your recovery passphrase.",
"messaging by entering your Security Phrase.",
)}</p>
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
@ -371,8 +371,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
/>
</form>
{_t(
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
"If you've forgotten your Security Phrase you can "+
"<button1>use your Security Key</button1> or " +
"<button2>set up new recovery options</button2>"
, {}, {
button1: s => <AccessibleButton className="mx_linkButton"
@ -390,7 +390,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
})}
</div>;
} else {
title = _t("Enter recovery key");
title = _t("Enter Security Key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -399,11 +399,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
{"\uD83D\uDC4D "}{_t("This looks like a valid Security Key!")}
</div>;
} else {
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
{"\uD83D\uDC4E "}{_t("Not a valid Security Key")}
</div>;
}
@ -415,7 +415,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
)}</p>
<p>{_t(
"Access your secure message history and set up secure " +
"messaging by entering your recovery key.",
"messaging by entering your Security Key.",
)}</p>
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
@ -434,7 +434,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
/>
</div>
{_t(
"If you've forgotten your recovery key you can "+
"If you've forgotten your Security Key you can "+
"<button>set up new recovery options</button>"
, {}, {
button: s => <AccessibleButton className="mx_linkButton"

View File

@ -124,7 +124,7 @@ export default class EditableItemList extends React.Component {
<Field label={this.props.placeholder} type="text"
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
list={this.props.suggestionsListId} />
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
{_t("Add")}
</AccessibleButton>
</form>

View File

@ -165,6 +165,7 @@ export default class PersistedElement extends React.Component {
const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, {
zIndex: 9,
position: 'absolute',
top: parentRect.top + 'px',
left: parentRect.left + 'px',

View File

@ -1,7 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,23 +23,11 @@ import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+]).*?)(?=\/|\?|$)/;
class Pill extends React.Component {
static isPillUrl(url) {
return !!getPrimaryPermalinkEntity(url);
}
static isMessagePillUrl(url) {
return !!REGEX_LOCAL_PERMALINK.exec(url);
}
static roomNotifPos(text) {
return text.indexOf("@room");
}
@ -56,7 +44,7 @@ class Pill extends React.Component {
static propTypes = {
// The Type of this Pill. If url is given, this is auto-detected.
type: PropTypes.string,
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
// The URL to pillify (no validation is done)
url: PropTypes.string,
// Whether the pill is in a message
inMessage: PropTypes.bool,
@ -90,12 +78,9 @@ class Pill extends React.Component {
if (nextProps.url) {
if (nextProps.inMessage) {
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = REGEX_LOCAL_PERMALINK.exec(nextProps.url) || [];
resourceId = matrixToMatch[1]; // The room/user ID
prefix = matrixToMatch[2]; // The first character of prefix
const parts = parseAppLocalLink(nextProps.url);
resourceId = parts.primaryEntityId; // The room/user ID
prefix = parts.sigil; // The first character of prefix
} else {
resourceId = getPrimaryPermalinkEntity(nextProps.url);
prefix = resourceId ? resourceId[0] : undefined;

View File

@ -15,19 +15,40 @@ limitations under the License.
*/
import React from "react";
import { chunk } from "lodash";
import classNames from "classnames";
import {MatrixClient} from "matrix-js-sdk/src/client";
import PlatformPeg from "../../../PlatformPeg";
import AccessibleButton from "./AccessibleButton";
import {_t} from "../../../languageHandler";
import {IIdentityProvider, ISSOFlow} from "../../../Login";
import classNames from "classnames";
import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
interface ISSOButtonProps extends Omit<IProps, "flow"> {
idp: IIdentityProvider;
mini?: boolean;
}
const getIcon = (brand: IdentityProviderBrand | string) => {
switch (brand) {
case IdentityProviderBrand.Apple:
return require(`../../../../res/img/element-icons/brands/apple.svg`);
case IdentityProviderBrand.Facebook:
return require(`../../../../res/img/element-icons/brands/facebook.svg`);
case IdentityProviderBrand.Github:
return require(`../../../../res/img/element-icons/brands/github.svg`);
case IdentityProviderBrand.Gitlab:
return require(`../../../../res/img/element-icons/brands/gitlab.svg`);
case IdentityProviderBrand.Google:
return require(`../../../../res/img/element-icons/brands/google.svg`);
case IdentityProviderBrand.Twitter:
return require(`../../../../res/img/element-icons/brands/twitter.svg`);
default:
return null;
}
}
const SSOButton: React.FC<ISSOButtonProps> = ({
matrixClient,
loginType,
@ -37,7 +58,6 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
mini,
...props
}) => {
const kind = primary ? "primary" : "primary_outline";
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
const onClick = () => {
@ -45,30 +65,35 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
};
let icon;
if (typeof idp?.icon === "string" && (idp.icon.startsWith("mxc://") || idp.icon.startsWith("https://"))) {
icon = <img
src={matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true)}
height="24"
width="24"
alt={label}
/>;
let brandClass;
const brandIcon = idp ? getIcon(idp.brand) : null;
if (brandIcon) {
const brandName = idp.brand.split(".").pop();
brandClass = `mx_SSOButton_brand_${brandName}`;
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
icon = <img src={src} height="24" width="24" alt={idp.name} />;
}
const classes = classNames("mx_SSOButton", {
[brandClass]: brandClass,
mx_SSOButton_mini: mini,
mx_SSOButton_default: !idp,
mx_SSOButton_primary: primary,
});
if (mini) {
// TODO fallback icon
return (
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
<AccessibleTooltipButton {...props} title={label} className={classes} onClick={onClick}>
{ icon }
</AccessibleButton>
</AccessibleTooltipButton>
);
}
return (
<AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
<AccessibleButton {...props} className={classes} onClick={onClick}>
{ icon }
{ label }
</AccessibleButton>
@ -83,6 +108,8 @@ interface IProps {
primary?: boolean;
}
const MAX_PER_ROW = 6;
const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
const providers = flow["org.matrix.msc2858.identity_providers"] || [];
if (providers.length < 2) {
@ -97,17 +124,24 @@ const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAf
</div>;
}
const rows = Math.ceil(providers.length / MAX_PER_ROW);
const size = Math.ceil(providers.length / rows);
return <div className="mx_SSOButtons">
{ providers.map(idp => (
<SSOButton
key={idp.id}
matrixClient={matrixClient}
loginType={loginType}
fragmentAfterLogin={fragmentAfterLogin}
idp={idp}
mini={true}
primary={primary}
/>
{ chunk(providers, size).map(chunk => (
<div key={chunk[0].id} className="mx_SSOButtons_row">
{ chunk.map(idp => (
<SSOButton
key={idp.id}
matrixClient={matrixClient}
loginType={loginType}
fragmentAfterLogin={fragmentAfterLogin}
idp={idp}
mini={true}
primary={primary}
/>
)) }
</div>
)) }
</div>;
};

View File

@ -19,6 +19,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import WidgetStore from "../../../stores/WidgetStore";
import EventTileBubble from "./EventTileBubble";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
interface IProps {
mxEvent: MatrixEvent;
@ -33,10 +35,15 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
const url = this.props.mxEvent.getContent()['url'];
const prevUrl = this.props.mxEvent.getPrevContent()['url'];
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const widgetId = this.props.mxEvent.getStateKey();
const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find(w => w.id === widgetId);
let joinCopy = _t('Join the conference at the top of this room');
if (!WidgetStore.instance.isPinned(this.props.mxEvent.getRoomId(), this.props.mxEvent.getStateKey())) {
if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) {
joinCopy = _t('Join the conference from the room information card on the right');
} else if (!widget) {
joinCopy = null;
}
if (!url) {

View File

@ -37,13 +37,14 @@ import SettingsStore from "../../../settings/SettingsStore";
import TextWithTooltip from "../elements/TextWithTooltip";
import WidgetAvatar from "../avatars/WidgetAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import WidgetStore, {IApp, MAX_PINNED} from "../../../stores/WidgetStore";
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
import { E2EStatus } from "../../../utils/ShieldUtils";
import RoomContext from "../../../contexts/RoomContext";
import {UIFeature} from "../../../settings/UIFeature";
import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
interface IProps {
room: Room;
@ -76,8 +77,9 @@ export const useWidgets = (room: Room) => {
setApps([...WidgetStore.instance.getApps(room.roomId)]);
}, [room]);
useEffect(updateApps, [room]);
useEffect(updateApps, [room, updateApps]);
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateApps);
return apps;
};
@ -102,10 +104,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
});
};
const isPinned = WidgetStore.instance.isPinned(room.roomId, app.id);
const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
const togglePin = isPinned
? () => { WidgetStore.instance.unpinWidget(room.roomId, app.id); }
: () => { WidgetStore.instance.pinWidget(room.roomId, app.id); };
? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); }
: () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top); };
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
let contextMenu;
@ -120,7 +122,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
/>;
}
const cannotPin = !isPinned && !WidgetStore.instance.canPin(room.roomId, app.id);
const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, Container.Top);
let pinTitle: string;
if (cannotPin) {
@ -184,9 +186,18 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
}
};
let copyLayoutBtn = null;
if (apps.length > 0 && WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
copyLayoutBtn = (
<AccessibleButton kind="link" onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
{ _t("Set my room layout for everyone") }
</AccessibleButton>
);
}
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
{ apps.map(app => <AppRow key={app.id} app={app} room={room} />) }
{ copyLayoutBtn }
<AccessibleButton kind="link" onClick={onManageIntegrations}>
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
</AccessibleButton>

View File

@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext, useEffect} from "react";
import {Room} from "matrix-js-sdk/src/models/room";
import React, { useContext, useEffect } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import BaseCard from "./BaseCard";
import WidgetUtils from "../../../utils/WidgetUtils";
import AppTile from "../elements/AppTile";
import {_t} from "../../../languageHandler";
import {useWidgets} from "./RoomSummaryCard";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import { _t } from "../../../languageHandler";
import { useWidgets } from "./RoomSummaryCard";
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import {Action} from "../../../dispatcher/actions";
import WidgetStore from "../../../stores/WidgetStore";
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import { Action } from "../../../dispatcher/actions";
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
interface IProps {
room: Room;
@ -42,7 +42,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
const apps = useWidgets(room);
const app = apps.find(a => a.id === widgetId);
const isPinned = app && WidgetStore.instance.isPinned(room.roomId, app.id);
const isPinned = app && WidgetLayoutStore.instance.isInContainer(room, app, Container.Top);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();

View File

@ -120,17 +120,21 @@ export default class RoomProfileSettings extends React.Component {
};
_onDisplayNameChanged = (e) => {
this.setState({
displayName: e.target.value,
enableProfileSave: true,
});
this.setState({displayName: e.target.value});
if (this.state.originalDisplayName === e.target.value) {
this.setState({enableProfileSave: false});
} else {
this.setState({enableProfileSave: true});
}
};
_onTopicChanged = (e) => {
this.setState({
topic: e.target.value,
enableProfileSave: true,
});
this.setState({topic: e.target.value});
if (this.state.originalTopic === e.target.value) {
this.setState({enableProfileSave: false});
} else {
this.setState({enableProfileSave: true});
}
};
_onAvatarChanged = (e) => {
@ -158,31 +162,10 @@ export default class RoomProfileSettings extends React.Component {
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
return (
<form
onSubmit={this._saveProfile}
autoComplete="off"
noValidate={true}
className="mx_ProfileSettings_profileForm"
>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<Field label={_t("Room Name")}
type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} />
<Field id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
type="text" value={this.state.topic} autoComplete="off"
onChange={this._onTopicChanged} element="textarea" />
</div>
<AvatarSetting
avatarUrl={this.state.avatarUrl}
avatarName={this.state.displayName || this.props.roomId}
avatarAltText={_t("Room avatar")}
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
</div>
let profileSettingsButtons;
if (this.state.canSetTopic && this.state.canSetName) {
profileSettingsButtons = (
<div className="mx_ProfileSettings_buttons">
<AccessibleButton
onClick={this._clearProfile}
@ -199,6 +182,35 @@ export default class RoomProfileSettings extends React.Component {
{_t("Save")}
</AccessibleButton>
</div>
);
}
return (
<form
onSubmit={this._saveProfile}
autoComplete="off"
noValidate={true}
className="mx_ProfileSettings_profileForm"
>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<Field label={_t("Room Name")}
type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} disabled={!this.state.canSetName} />
<Field className="mx_ProfileSettings_controls_topic" id="profileTopic" label={_t("Room Topic")} disabled={!this.state.canSetTopic}
type="text" value={this.state.topic} autoComplete="off"
onChange={this._onTopicChanged} element="textarea" />
</div>
<AvatarSetting
avatarUrl={this.state.avatarUrl}
avatarName={this.state.displayName || this.props.roomId}
avatarAltText={_t("Room avatar")}
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
</div>
{ profileSettingsButtons }
</form>
);
}

View File

@ -28,12 +28,13 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import WidgetStore from "../../../stores/WidgetStore";
import ResizeHandle from "../elements/ResizeHandle";
import Resizer from "../../../resizer/resizer";
import PercentageDistributor from "../../../resizer/distributors/percentage";
import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayoutStore";
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
import {useStateCallback} from "../../../hooks/useStateCallback";
export default class AppsDrawer extends React.Component {
static propTypes = {
@ -62,13 +63,13 @@ export default class AppsDrawer extends React.Component {
componentDidMount() {
ScalarMessaging.startListening();
WidgetStore.instance.on(this.props.room.roomId, this._updateApps);
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
ScalarMessaging.stopListening();
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
if (this._resizeContainer) {
this.resizer.detach();
@ -102,11 +103,10 @@ export default class AppsDrawer extends React.Component {
},
onResizeStop: () => {
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
// persist to localStorage
localStorage.setItem(this._getStorageKey(), JSON.stringify([
this.state.apps.map(app => app.id),
...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
]));
WidgetLayoutStore.instance.setResizerDistributions(
this.props.room, Container.Top,
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
);
},
};
// pass a truthy container for now, we won't call attach until we update it
@ -128,8 +128,6 @@ export default class AppsDrawer extends React.Component {
this._loadResizerPreferences();
};
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
componentDidUpdate(prevProps, prevState) {
@ -147,24 +145,16 @@ export default class AppsDrawer extends React.Component {
};
_loadResizerPreferences = () => {
try {
const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
// Every app was included in the last split, reuse the last sizes
if (this.state.apps.length <= lastIds.length && this.state.apps.every((app, i) => lastIds[i] === app.id)) {
sizes.forEach((size, i) => {
const distributor = this.resizer.forHandleAt(i);
if (distributor) {
distributor.size = size;
distributor.finish();
}
});
return;
}
} catch (e) {
// this is expected
}
if (this.state.apps) {
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
distributions.forEach((size, i) => {
const distributor = this.resizer.forHandleAt(i);
if (distributor) {
distributor.size = size;
distributor.finish();
}
});
} else if (this.state.apps) {
const distributors = this.resizer.getDistributors();
distributors.forEach(d => d.item.clearSize());
distributors.forEach(d => d.start());
@ -190,7 +180,7 @@ export default class AppsDrawer extends React.Component {
}
};
_getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
_getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
_updateApps = () => {
this.setState({
@ -248,10 +238,11 @@ export default class AppsDrawer extends React.Component {
return (
<div className={classes}>
<PersistentVResizer
id={"apps-drawer_" + this.props.room.roomId}
room={this.props.room}
minHeight={100}
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
handleClass="mx_AppsContainer_resizerHandle"
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}
>
@ -272,7 +263,7 @@ export default class AppsDrawer extends React.Component {
}
const PersistentVResizer = ({
id,
room,
minHeight,
maxHeight,
className,
@ -281,7 +272,24 @@ const PersistentVResizer = ({
resizeNotifier,
children,
}) => {
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
if (!minHeight) minHeight = 100;
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
// Convert from percentage to height. Note that the default height is 280px.
if (defaultHeight) {
defaultHeight = clamp(defaultHeight, 0, 100);
defaultHeight = percentageWithin(defaultHeight / 100, minHeight, maxHeight);
} else {
defaultHeight = 280;
}
const [height, setHeight] = useStateCallback(defaultHeight, newHeight => {
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
});
return <Resizable
size={{height: Math.min(height, maxHeight)}}

View File

@ -519,7 +519,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private async tabCompleteName(event: React.KeyboardEvent) {
try {
await new Promise(resolve => this.setState({showVisualBell: false}, resolve));
await new Promise<void>(resolve => this.setState({showVisualBell: false}, resolve));
const {model} = this.props;
const caret = this.getCaret();
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);

View File

@ -37,6 +37,7 @@ import {E2E_STATE} from "./E2EIcon";
import {toRem} from "../../../utils/units";
import {WidgetType} from "../../../widgets/WidgetType";
import RoomAvatar from "../avatars/RoomAvatar";
import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
@ -65,6 +66,7 @@ const stateEventTileTypes = {
'm.room.server_acl': 'messages.TextualEvent',
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
'im.vector.modular.widgets': 'messages.TextualEvent',
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
'm.room.tombstone': 'messages.TextualEvent',
'm.room.join_rules': 'messages.TextualEvent',
'm.room.guest_access': 'messages.TextualEvent',

View File

@ -109,9 +109,12 @@ function HangupButton(props) {
dis.dispatch({
action,
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId,
// hangup the call for this room. NB. We use the room in props as the room ID
// as call.roomId may be the 'virtual room', and the dispatch actions always
// use the user-facing room (there was a time when we deliberately used
// call.roomId and *not* props.roomId, but that was for the old
// style Freeswitch conference calls and those times are gone.)
room_id: props.roomId,
});
};

View File

@ -455,8 +455,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const unfilteredLists = RoomListStore.instance.unfilteredLists
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) {
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
<AccessibleButton

View File

@ -29,7 +29,7 @@ import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -51,7 +51,6 @@ import IconizedContextMenu, {
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps {
room: Room;
@ -99,12 +98,23 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
this.dispatcherRef = defaultDispatcher.register(this.onAction);
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
MessagePreviewStore.instance.on(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.roomProps = EchoChamber.forRoom(this.props.room);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
CommunityPrototypeStore.instance.on(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
);
this.props.room.on("Room.name", this.onRoomNameUpdate);
}
private onRoomNameUpdate = (room) => {
this.forceUpdate();
}
private onNotificationUpdate = () => {
@ -128,6 +138,26 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
this.setState({messagePreview: this.generatePreview()});
}
if (prevProps.room?.roomId !== this.props.room?.roomId) {
MessagePreviewStore.instance.off(
MessagePreviewStore.getPreviewChangedEventName(prevProps.room),
this.onRoomPreviewChanged,
);
MessagePreviewStore.instance.on(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(prevProps.room?.roomId),
this.onCommunityUpdate,
);
CommunityPrototypeStore.instance.on(
CommunityPrototypeStore.getUpdateEventName(this.props.room?.roomId),
this.onCommunityUpdate,
);
prevProps.room?.off("Room.name", this.onRoomNameUpdate);
this.props.room?.on("Room.name", this.onRoomNameUpdate);
}
}
public componentDidMount() {
@ -140,11 +170,18 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
public componentWillUnmount() {
if (this.props.room) {
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
MessagePreviewStore.instance.off(
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
);
this.props.room.off("Room.name", this.onRoomNameUpdate);
}
defaultDispatcher.unregister(this.dispatcherRef);
MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
}
private onAction = (payload: ActionPayload) => {

View File

@ -156,13 +156,14 @@ export default class SendMessageComposer extends React.Component {
this.onVerticalArrow(event, true);
} else if (event.key === Key.ARROW_DOWN) {
this.onVerticalArrow(event, false);
} else if (this._prepareToEncrypt) {
this._prepareToEncrypt();
} else if (event.key === Key.ESCAPE) {
dis.dispatch({
action: 'reply_to_event',
event: null,
});
} else if (this._prepareToEncrypt) {
// This needs to be last!
this._prepareToEncrypt();
}
};

View File

@ -81,7 +81,7 @@ export default class WhoIsTypingTile extends React.Component {
};
onRoomTimeline = (event, room) => {
if (room && room.roomId === this.props.room.roomId) {
if (room?.roomId === this.props.room?.roomId) {
const userId = event.getSender();
// remove user from usersTyping
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);

View File

@ -65,7 +65,7 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
const avatarClasses = classNames({
"mx_AvatarSetting_avatar": true,
"mx_AvatarSetting_avatar_hovering": isHovering,
"mx_AvatarSetting_avatar_hovering": isHovering && uploadAvatar,
});
return <div className={avatarClasses}>
{avatarElement}

View File

@ -1,115 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {_t} from "../../../languageHandler";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import Pill from "../elements/Pill";
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import BaseAvatar from "../avatars/BaseAvatar";
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
@replaceableComponent("views.settings.BridgeTile")
export default class BridgeTile extends React.PureComponent {
static propTypes = {
ev: PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
}
state = {
visible: false,
}
_toggleVisible() {
this.setState({
visible: !this.state.visible,
});
}
render() {
const content = this.props.ev.getContent();
const { channel, network, protocol } = content;
const protocolName = protocol.displayname || protocol.id;
const channelName = channel.displayname || channel.id;
const networkName = network ? network.displayname || network.id : protocolName;
let creator = null;
if (content.creator) {
creator = _t("This bridge was provisioned by <user />.", {}, {
user: <Pill
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(content.creator)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
});
}
const bot = _t("This bridge is managed by <user />.", {}, {
user: <Pill
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(this.props.ev.getSender())}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
});
let networkIcon;
if (protocol.avatar) {
const avatarUrl = getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
protocol.avatar, 64, 64, "crop",
);
networkIcon = <BaseAvatar className="protocol-icon"
width={48}
height={48}
resizeMethod='crop'
name={ protocolName }
idName={ protocolName }
url={ avatarUrl }
/>;
} else {
networkIcon = <div class="noProtocolIcon"></div>;
}
const id = this.props.ev.getId();
const metadataClassname = "metadata" + (this.state.visible ? " visible" : "");
return (<li key={id}>
<div className="column-icon">
{networkIcon}
</div>
<div className="column-data">
<h3>{protocolName}</h3>
<p className="workspace-channel-details">
<span>{_t("Workspace: %(networkName)s", {networkName})}</span>
<span className="channel">{_t("Channel: %(channelName)s", {channelName})}</span>
</p>
<p className={metadataClassname}>
{creator} {bot}
</p>
<AccessibleButton className="mx_showMore" kind="secondary" onClick={this._toggleVisible.bind(this)}>
{ this.state.visible ? _t("Show less") : _t("Show more") }
</AccessibleButton>
</div>
</li>);
}
}

View File

@ -0,0 +1,167 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {_t} from "../../../languageHandler";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import Pill from "../elements/Pill";
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import BaseAvatar from "../avatars/BaseAvatar";
import SettingsStore from "../../../settings/SettingsStore";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { isUrlPermitted } from '../../../HtmlUtils';
interface IProps {
ev: MatrixEvent;
room: Room;
}
/**
* This should match https://github.com/matrix-org/matrix-doc/blob/hs/msc-bridge-inf/proposals/2346-bridge-info-state-event.md#mbridge
*/
interface IBridgeStateEvent {
bridgebot: string;
creator?: string;
protocol: {
id: string;
displayname?: string;
// eslint-disable-next-line camelcase
avatar_url?: string;
// eslint-disable-next-line camelcase
external_url?: string;
};
network?: {
id: string;
displayname?: string;
// eslint-disable-next-line camelcase
avatar_url?: string;
// eslint-disable-next-line camelcase
external_url?: string;
};
channel: {
id: string;
displayname?: string;
// eslint-disable-next-line camelcase
avatar_url?: string;
// eslint-disable-next-line camelcase
external_url?: string;
};
}
export default class BridgeTile extends React.PureComponent<IProps> {
static propTypes = {
ev: PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
}
render() {
const content: IBridgeStateEvent = this.props.ev.getContent();
// Validate
if (!content.channel?.id || !content.protocol?.id) {
console.warn(`Bridge info event ${this.props.ev.getId()} has missing content. Tile will not render`);
return null;
}
if (!content.bridgebot) {
// Bridgebot was not required previously, so in order to not break rooms we are allowing
// the sender to be used in place. When the proposal is merged, this should be removed.
console.warn(`Bridge info event ${this.props.ev.getId()} does not provide a 'bridgebot' key which`
+ "is deprecated behaviour. Using sender for now.");
content.bridgebot = this.props.ev.getSender();
}
const { channel, network, protocol } = content;
const protocolName = protocol.displayname || protocol.id;
const channelName = channel.displayname || channel.id;
let creator = null;
if (content.creator) {
creator = <li>{_t("This bridge was provisioned by <user />.", {}, {
user: () => <Pill
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(content.creator)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
})}</li>;
}
const bot = <li>{_t("This bridge is managed by <user />.", {}, {
user: () => <Pill
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(content.bridgebot)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
})}</li>;
let networkIcon;
if (protocol.avatar_url) {
const avatarUrl = getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
protocol.avatar_url, 64, 64, "crop",
);
networkIcon = <BaseAvatar className="protocol-icon"
width={48}
height={48}
resizeMethod='crop'
name={ protocolName }
idName={ protocolName }
url={ avatarUrl }
/>;
} else {
networkIcon = <div className="noProtocolIcon"></div>;
}
let networkItem = null;
if (network) {
const networkName = network.displayname || network.id;
let networkLink = <span>{networkName}</span>;
if (typeof network.external_url === "string" && isUrlPermitted(network.external_url)) {
networkLink = <a href={network.external_url} target="_blank" rel="noreferrer noopener">{networkName}</a>
}
networkItem = _t("Workspace: <networkLink/>", {}, {
networkLink: () => networkLink,
});
}
let channelLink = <span>{channelName}</span>;
if (typeof channel.external_url === "string" && isUrlPermitted(channel.external_url)) {
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{channelName}</a>
}
const id = this.props.ev.getId();
return (<li key={id}>
<div className="column-icon">
{networkIcon}
</div>
<div className="column-data">
<h3>{protocolName}</h3>
<p className="workspace-channel-details">
{networkItem}
<span className="channel">{_t("Channel: <channelLink/>", {}, {
channelLink: () => channelLink,
})}</span>
</p>
<ul className="metadata">
{creator} {bot}
</ul>
</div>
</li>);
}
}

View File

@ -424,7 +424,7 @@ export default class SecureBackupPanel extends React.PureComponent {
<p>{_t(
"Back up your encryption keys with your account data in case you " +
"lose access to your sessions. Your keys will be secured with a " +
"unique Recovery Key.",
"unique Security Key.",
)}</p>
{statusDescription}
<details>

View File

@ -52,6 +52,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
'showImages',
'showChatEffects',
'Pill.shouldShowPillAvatar',
'ctrlFForSearch',
];
static GENERAL_SETTINGS = [

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, CSSProperties, ReactNode } from 'react';
import React, { createRef, CSSProperties } from 'react';
import dis from '../../../dispatcher/dispatcher';
import CallHandler from '../../../CallHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -212,9 +212,10 @@ export default class CallView extends React.Component<IProps, IState> {
};
private onExpandClick = () => {
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
dis.dispatch({
action: 'view_room',
room_id: this.props.call.roomId,
room_id: userFacingRoomId,
});
};
@ -340,27 +341,33 @@ export default class CallView extends React.Component<IProps, IState> {
};
private onRoomAvatarClick = () => {
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
dis.dispatch({
action: 'view_room',
room_id: this.props.call.roomId,
room_id: userFacingRoomId,
});
}
private onSecondaryRoomAvatarClick = () => {
const userFacingRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
dis.dispatch({
action: 'view_room',
room_id: this.props.secondaryCall.roomId,
room_id: userFacingRoomId,
});
}
private onCallResumeClick = () => {
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
}
public render() {
const client = MatrixClientPeg.get();
const callRoom = client.getRoom(this.props.call.roomId);
const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null;
const callRoomId = CallHandler.roomIdForCall(this.props.call);
const secondaryCallRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
const callRoom = client.getRoom(callRoomId);
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
let dialPad;
let contextMenu;
@ -456,7 +463,7 @@ export default class CallView extends React.Component<IProps, IState> {
onClick={() => {
dis.dispatch({
action: 'hangup',
room_id: this.props.call.roomId,
room_id: callRoomId,
});
}}
/>
@ -487,6 +494,7 @@ export default class CallView extends React.Component<IProps, IState> {
}
if (this.props.call.type === CallType.Video) {
let localVideoFeed = null;
let onHoldContent = null;
let onHoldBackground = null;
const backgroundStyle: CSSProperties = {};
@ -505,6 +513,9 @@ export default class CallView extends React.Component<IProps, IState> {
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
}
if (!this.state.vidMuted) {
localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />;
}
// if we're fullscreen, we don't want to set a maxHeight on the video element.
const maxVideoHeight = getFullScreenElement() ? null : (
@ -520,7 +531,7 @@ export default class CallView extends React.Component<IProps, IState> {
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize}
maxHeight={maxVideoHeight}
/>
<VideoFeed type={VideoFeedType.Local} call={this.props.call} />
{localVideoFeed}
{onHoldContent}
{callControls}
</div>;

View File

@ -70,7 +70,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
e.stopPropagation();
dis.dispatch({
action: 'answer',
room_id: this.state.incomingCall.roomId,
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
});
};
@ -78,7 +78,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
e.stopPropagation();
dis.dispatch({
action: 'reject',
room_id: this.state.incomingCall.roomId,
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
});
};
@ -89,7 +89,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
let room = null;
if (this.state.incomingCall) {
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall));
}
const caller = room ? room.name : _t("Unknown caller");

View File

@ -18,12 +18,12 @@ import {useEffect, useState} from "react";
import SettingsStore from '../settings/SettingsStore';
// Hook to fetch the value of a setting and dynamically update when it changes
export const useSettingValue = (settingName: string, roomId: string = null, excludeDefault = false) => {
const [value, setValue] = useState(SettingsStore.getValue(settingName, roomId, excludeDefault));
export const useSettingValue = <T>(settingName: string, roomId: string = null, excludeDefault = false) => {
const [value, setValue] = useState(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(SettingsStore.getValue(settingName, roomId, excludeDefault));
setValue(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
});
// clean-up
return () => {

View File

@ -0,0 +1,28 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {Dispatch, SetStateAction, useState} from "react";
// Hook to simplify interactions with a store-backed state values
// Returns value and method to change the state value
export const useStateCallback = <T>(initialValue: T, callback: (v: T) => void): [T, Dispatch<SetStateAction<T>>] => {
const [value, setValue] = useState(initialValue);
const interceptSetValue = (newVal: T) => {
setValue(newVal);
callback(newVal);
};
return [value, interceptSetValue];
};

View File

@ -948,5 +948,7 @@
"Confirm adding phone number": "Confirma l'addició del número de telèfon",
"Add Email Address": "Afegeix una adreça de correu electrònic",
"Confirm": "Confirma",
"Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic."
"Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic.",
"Unable to access webcam / microphone": "No s'ha pogut accedir a la càmera web / micròfon",
"Unable to access microphone": "No s'ha pogut accedir al micròfon"
}

View File

@ -2064,7 +2064,7 @@
"%(networkName)s rooms": "místnosti v %(networkName)s",
"Matrix rooms": "místnosti na Matrixu",
"Enable end-to-end encryption": "Povolit E2E šifrování",
"You cant disable this later. Bridges & most bots wont work yet.": "Už to v budoucnu nepůjde vypnout. Většina botů a propojení zatím nefunguje.",
"You cant disable this later. Bridges & most bots wont work yet.": "Toto nelze později vypnout. Většina botů a propojení zatím nefunguje.",
"Server did not require any authentication": "Server nevyžadoval žádné ověření",
"Server did not return valid authentication information.": "Server neposkytl platné informace o ověření.",
"Confirm your account deactivation by using Single Sign On to prove your identity.": "Potvrďte deaktivaci účtu použtím Jednotného přihlášení.",
@ -2306,7 +2306,7 @@
"Backup version:": "Verze zálohy:",
"Algorithm:": "Algoritmus:",
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Tuto možnost můžete povolit, pokud bude místnost použita pouze pro spolupráci s interními týmy na vašem domovském serveru. Toto nelze později změnit.",
"Block anyone not part of %(serverName)s from ever joining this room.": "Blokovat komukoli, kdo není součástí serveru %(serverName)s, aby se nikdy nepřipojil do této místnosti.",
"Block anyone not part of %(serverName)s from ever joining this room.": "Blokovat komukoli, kdo není součástí serveru %(serverName)s, aby se připojil do této místnosti.",
"Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Zálohujte šifrovací klíče s daty vašeho účtu pro případ, že ztratíte přístup k relacím. Vaše klíče budou zabezpečeny jedinečným klíčem pro obnovení.",
"Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Níže můžete spravovat názvy a odhlásit se ze svých relací nebo <a>je ověřit v uživatelském profilu</a>.",
"or another cross-signing capable Matrix client": "nebo jiný Matrix klient schopný cross-signing",
@ -2904,5 +2904,53 @@
"Start a Conversation": "Zahájit konverzaci",
"Dial pad": "Číselník",
"There was an error looking up the phone number": "Při vyhledávání telefonního čísla došlo k chybě",
"Unable to look up phone number": "Nelze nalézt telefonní číslo"
"Unable to look up phone number": "Nelze nalézt telefonní číslo",
"Channel: <channelLink/>": "Kanál: <channelLink/>",
"Change which room, message, or user you're viewing": "Změňte, kterou místnost, zprávu nebo uživatele si prohlížíte",
"Workspace: <networkLink/>": "Pracovní oblast: <networkLink/>",
"If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Pokud jste zapomněli bezpečnostní klíč, můžete <button>nastavit nové možnosti obnovení</button>",
"If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Pokud jste zapomněli bezpečnostní frázi, můžete <button1>použít bezpečnostní klíč</button1> nebo <button2>nastavit nové možnosti obnovení</button2>",
"Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "Zálohu nebylo možné dešifrovat pomocí této bezpečnostní fráze: ověřte, zda jste zadali správnou bezpečnostní frázi.",
"Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "Zálohu nebylo možné dešifrovat pomocí tohoto bezpečnostního klíče: ověřte, zda jste zadali správný bezpečnostní klíč.",
"Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Zálohujte šifrovací klíče s daty účtu pro případ, že ztratíte přístup k relacím. Vaše klíče budou zabezpečeny jedinečným bezpečnostním klíčem.",
"Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Váš bezpečnostní klíč je bezpečnostní síť - můžete ji použít k obnovení přístupu k šifrovaným zprávám, pokud zapomenete bezpečnostní frázi.",
"We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Na náš server uložíme zašifrovanou kopii vašich klíčů. Zabezpečte zálohu pomocí bezpečnostní fráze.",
"Access your secure message history and set up secure messaging by entering your Security Key.": "Vstupte do historie zabezpečených zpráv a nastavte zabezpečené zprávy zadáním bezpečnostního klíče.",
"Access your secure message history and set up secure messaging by entering your Security Phrase.": "Vstupte do historie zabezpečených zpráv a nastavte zabezpečené zprávy zadáním bezpečnostní fráze.",
"Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Nelze získat přístup k zabezpečenému úložišti. Ověřte, zda jste zadali správnou bezpečnostní frázi.",
"We recommend you change your password and Security Key in Settings immediately": "Doporučujeme vám okamžitě změnit heslo a bezpečnostní klíč v Nastavení",
"This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Tato relace zjistila, že byla odstraněna vaše bezpečnostní fráze a klíč pro zabezpečené zprávy.",
"A new Security Phrase and key for Secure Messages have been detected.": "Byla zjištěna nová bezpečnostní fráze a klíč pro zabezpečené zprávy.",
"Make a copy of your Security Key": "Vytvořte kopii bezpečnostního klíče",
"Confirm your Security Phrase": "Potvrďte svou bezpečnostní frázi",
"Secure your backup with a Security Phrase": "Zabezpečte zálohu pomocí bezpečnostní fráze",
"Repeat your Security Phrase...": "Zopakujte vaši bezpečnostní frázi...",
"Set up with a Security Key": "Nastavit pomocí bezpečnostního klíče",
"Use Security Key": "Použít bezpečnostní klíč",
"This looks like a valid Security Key!": "Vypadá to jako platný bezpečnostní klíč!",
"Invalid Security Key": "Neplatný bezpečnostní klíč",
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Váš bezpečnostní klíč byl <b>zkopírován do schránky</b>, vložte jej do:",
"Your Security Key is in your <b>Downloads</b> folder.": "Váš bezpečnostní klíč je ve složce <b>Stažené soubory</b>.",
"Your Security Key": "Váš bezpečnostní klíč",
"Please enter your Security Phrase a second time to confirm.": "Potvrďte prosím svou bezpečnostní frázi.",
"Great! This Security Phrase looks strong enough.": "Skvělé! Tato bezpečnostní fráze vypadá dostatečně silně.",
"Use Security Key or Phrase": "Použijte bezpečnostní klíč nebo frázi",
"Not a valid Security Key": "Neplatný bezpečnostní klíč",
"Enter Security Key": "Zadejte bezpečnostní klíč",
"Enter Security Phrase": "Zadejte bezpečnostní frázi",
"Incorrect Security Phrase": "Nesprávná bezpečnostní fráze",
"Security Key mismatch": "Neshoda bezpečnostního klíče",
"Wrong Security Key": "Špatný bezpečnostní klíč",
"Set my room layout for everyone": "Nastavit všem rozložení mé místnosti",
"%(senderName)s has updated the widget layout": "%(senderName)s aktualizoval rozložení widgetu",
"Search (must be enabled)": "Hledat (musí být povoleno)",
"Remember this": "Zapamatujte si toto",
"The widget will verify your user ID, but won't be able to perform actions for you:": "Widget ověří vaše uživatelské ID, ale nebude za vás moci provádět akce:",
"Allow this widget to verify your identity": "Povolte tomuto widgetu ověřit vaši identitu",
"Use Ctrl + F to search": "Hledejte pomocí Ctrl + F",
"Use Command + F to search": "Hledejte pomocí Command + F",
"Converts the DM to a room": "Převede přímou zprávu na místnost",
"Converts the room to a DM": "Převede místnost na přímou zprávu",
"Mobile experience": "Zážitek na mobilních zařízeních",
"Element Web is currently experimental on mobile. The native apps are recommended for most people.": "Element Web je v současné době experimentální na mobilních zařízeních. Nativní aplikace se doporučují pro většinu lidí."
}

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