diff --git a/.babelrc b/.babelrc deleted file mode 100644 index abe7e1ef3f..0000000000 --- a/.babelrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "presets": [ - "react", - "es2015", - "es2016" - ], - "plugins": [ - [ - "transform-builtin-extend", - { - "globals": ["Error"] - } - ], - "transform-class-properties", - "transform-object-rest-spread", - "transform-runtime", - "add-module-exports", - "syntax-dynamic-import" - ] -} diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index be0d5e404c..a8ce1273fb 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -1,13 +1,60 @@ steps: - - label: ":eslint: Lint" + - label: ":eslint: JS Lint" command: - "echo '--- Install js-sdk'" - "./scripts/ci/install-deps.sh" - - "yarn lintwithexclusions" - - "yarn stylelint" + - "yarn lint:js" plugins: - docker#v3.0.1: - image: "node:10" + image: "node:12" + + - label: ":eslint: TS Lint" + command: + - "echo '--- Install js-sdk'" + - "./scripts/ci/install-deps.sh" + - "yarn lint:ts" + plugins: + - docker#v3.0.1: + image: "node:12" + + - label: ":eslint: Types Lint" + command: + - "echo '--- Install js-sdk'" + - "./scripts/ci/install-deps.sh" + - "yarn lint:types" + plugins: + - docker#v3.0.1: + image: "node:12" + + - label: ":jest: Tests" + agents: + # We use a medium sized instance instead of the normal small ones because + # webpack loves to gorge itself on resources. + queue: "medium" + command: + - "echo '--- Install js-sdk'" + # TODO: Remove hacky chmod for BuildKite + - "chmod +x ./scripts/ci/*.sh" + - "chmod +x ./scripts/*" + - "echo '--- Installing Dependencies'" + - "./scripts/ci/install-deps.sh" + - "echo '--- Running initial build steps'" + - "yarn build" + - "echo '+++ Running Tests'" + - "yarn test" + plugins: + - docker#v3.0.1: + image: "node:12" + + - label: "🛠 Build" + command: + - "echo '--- Install js-sdk'" + - "./scripts/ci/install-deps.sh" + - "echo '+++ Building Project'" + - "yarn build" + plugins: + - docker#v3.0.1: + image: "node:12" - label: ":chains: End-to-End Tests" agents: @@ -21,39 +68,15 @@ steps: - "chmod +x ./scripts/*" - "echo '--- Install js-sdk'" - "./scripts/ci/install-deps.sh" + - "echo '--- Running initial build steps'" + - "yarn build" + - "echo '+++ Running Tests'" - "./scripts/ci/end-to-end-tests.sh" plugins: - docker#v3.0.1: image: "matrixdotorg/riotweb-ci-e2etests-env:latest" propagate-environment: true - - label: ":karma: Tests" - agents: - # We use a medium sized instance instead of the normal small ones because - # webpack loves to gorge itself on resources. - queue: "medium" - command: - # Install chrome - - "echo '--- Installing Chrome'" - - "wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -" - - "sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'" - - "apt-get update" - - "apt-get install -y google-chrome-stable" - # Run tests - # TODO: Remove hacky chmod for BuildKite - - "chmod +x ./scripts/ci/*.sh" - - "chmod +x ./scripts/*" - - "echo '--- Installing Dependencies'" - - "./scripts/ci/install-deps.sh" - - "echo '+++ Running Tests'" - - "./scripts/ci/unit-tests.sh" - env: - CHROME_BIN: "/usr/bin/google-chrome-stable" - plugins: - - docker#v3.0.1: - image: "node:10" - propagate-environment: true - - label: "🔧 Riot Tests" agents: # We use a medium sized instance instead of the normal small ones because @@ -66,12 +89,13 @@ steps: - "sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'" - "apt-get update" - "apt-get install -y google-chrome-stable" - # Run tests # TODO: Remove hacky chmod for BuildKite - "chmod +x ./scripts/ci/*.sh" - "chmod +x ./scripts/*" - "echo '--- Installing Dependencies'" - "./scripts/ci/install-deps.sh" + - "echo '--- Running initial build steps'" + - "yarn build" - "echo '+++ Running Tests'" - "./scripts/ci/riot-unit-tests.sh" env: diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 7d998f8c4b..36b03b121c 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -33,7 +33,6 @@ src/components/views/rooms/RoomList.js src/components/views/rooms/RoomPreviewBar.js src/components/views/rooms/SearchBar.js src/components/views/rooms/SearchResultTile.js -src/components/views/rooms/SlateMessageComposer.js src/components/views/settings/ChangeAvatar.js src/components/views/settings/ChangePassword.js src/components/views/settings/DevicesPanel.js @@ -58,7 +57,6 @@ src/utils/Receipt.js src/Velociraptor.js test/components/structures/MessagePanel-test.js test/components/views/dialogs/InteractiveAuthDialog-test.js -test/components/views/rooms/MessageComposerInput-test.js test/mock-clock.js test/notifications/ContentRules-test.js test/notifications/PushRuleVectorState-test.js diff --git a/.eslintrc.js b/.eslintrc.js index 81c3752301..6a0576c58a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,7 +5,10 @@ const path = require('path'); // but only if they come from a module that starts with eslint-config- // So we load the filename directly (and it could be in node_modules/ // or or ../node_modules/ etc) -const matrixJsSdkPath = path.dirname(require.resolve('matrix-js-sdk')); +// +// We add a `..` to the end because the js-sdk lives out of lib/, but the eslint +// config is at the project root. +const matrixJsSdkPath = path.join(path.dirname(require.resolve('matrix-js-sdk')), '..'); module.exports = { parser: "babel-eslint", @@ -25,6 +28,7 @@ module.exports = { parserOptions: { ecmaFeatures: { jsx: true, + legacyDecorators: true, } }, rules: { diff --git a/CHANGELOG.md b/CHANGELOG.md index 5390cad319..3ae2711e25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,193 @@ +Changes in [1.7.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.6) (2020-01-13) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.6-rc.2...v1.7.6) + + * Repair community member info panel + [\#3834](https://github.com/matrix-org/matrix-react-sdk/pull/3834) + * Add feature flag around the presence indicator in room list + [\#3833](https://github.com/matrix-org/matrix-react-sdk/pull/3833) + +Changes in [1.7.6-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.6-rc.2) (2020-01-08) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.6-rc.1...v1.7.6-rc.2) + + * Strip all variation selectors on emoji + [\#3818](https://github.com/matrix-org/matrix-react-sdk/pull/3818) + +Changes in [1.7.6-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.6-rc.1) (2020-01-06) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.5...v1.7.6-rc.1) + + * Deduplicate recent emoji + [\#3806](https://github.com/matrix-org/matrix-react-sdk/pull/3806) + * Fix ability to remove avatars + [\#3803](https://github.com/matrix-org/matrix-react-sdk/pull/3803) + * Update from Weblate + [\#3810](https://github.com/matrix-org/matrix-react-sdk/pull/3810) + * User Info fetch latest RoomMember instead of showing historical data + [\#3788](https://github.com/matrix-org/matrix-react-sdk/pull/3788) + * Remove all usages of slate in favour of CIDER + [\#3808](https://github.com/matrix-org/matrix-react-sdk/pull/3808) + * Use display name when pinned messages are changed + [\#3809](https://github.com/matrix-org/matrix-react-sdk/pull/3809) + * Fix inverted diff line highlighting in dark theme + [\#3790](https://github.com/matrix-org/matrix-react-sdk/pull/3790) + * Bridge info settings tab + [\#3693](https://github.com/matrix-org/matrix-react-sdk/pull/3693) + * Send the labs flags the client is running with in rageshake + [\#3805](https://github.com/matrix-org/matrix-react-sdk/pull/3805) + * Initial implementation of FTUE user lists design + [\#3792](https://github.com/matrix-org/matrix-react-sdk/pull/3792) + * Update key backup creation and recovery paths for SSSS + [\#3800](https://github.com/matrix-org/matrix-react-sdk/pull/3800) + * Don't fail if logs exists and is an empty dir + [\#3798](https://github.com/matrix-org/matrix-react-sdk/pull/3798) + * Comment remaining non-cross-signing-compliant components + [\#3799](https://github.com/matrix-org/matrix-react-sdk/pull/3799) + * Remove 'unverify' from UserInfoPanel + [\#3797](https://github.com/matrix-org/matrix-react-sdk/pull/3797) + * Use deviceTrust when displaying key backup trust status + [\#3795](https://github.com/matrix-org/matrix-react-sdk/pull/3795) + * Don't crash if a keyshare request is removed + [\#3793](https://github.com/matrix-org/matrix-react-sdk/pull/3793) + * Convert /verify to checkDeviceTrust + [\#3794](https://github.com/matrix-org/matrix-react-sdk/pull/3794) + * Remove E2eIcon onClick + [\#3791](https://github.com/matrix-org/matrix-react-sdk/pull/3791) + * support channel names with slash in name/alias + [\#3778](https://github.com/matrix-org/matrix-react-sdk/pull/3778) + * Fix NPE when filtering the room list + [\#3787](https://github.com/matrix-org/matrix-react-sdk/pull/3787) + * Turn RoomAliasField into properly controlled and use in RoomSettings + [\#3782](https://github.com/matrix-org/matrix-react-sdk/pull/3782) + * fuzzy-sort MemberList + [\#3783](https://github.com/matrix-org/matrix-react-sdk/pull/3783) + * Serialize file uploads into room to match confirmation dialog order + [\#3786](https://github.com/matrix-org/matrix-react-sdk/pull/3786) + * Do not show Top Unread Messages Bar and Jump to bottom button if searching + [\#3785](https://github.com/matrix-org/matrix-react-sdk/pull/3785) + * Fix sticker picker chevron offset calculation + [\#3784](https://github.com/matrix-org/matrix-react-sdk/pull/3784) + * Fix not being able to promote others to the same power level as your own + [\#3781](https://github.com/matrix-org/matrix-react-sdk/pull/3781) + * Room Tile DMs online/active green dot + [\#3751](https://github.com/matrix-org/matrix-react-sdk/pull/3751) + * Fix spelling and grammar in README + [\#3780](https://github.com/matrix-org/matrix-react-sdk/pull/3780) + * Reintroduce working resizer code for right panel + [\#3776](https://github.com/matrix-org/matrix-react-sdk/pull/3776) + * Fix wrong scope binding on openHelp for TopLeftMenu + [\#3775](https://github.com/matrix-org/matrix-react-sdk/pull/3775) + * UserInfo hide kick/mute buttons if they make no sense + [\#3774](https://github.com/matrix-org/matrix-react-sdk/pull/3774) + * Fix duplicate Incoming Call prompt on Community Invite sublist + [\#3773](https://github.com/matrix-org/matrix-react-sdk/pull/3773) + * Apply new design to highlighted tags and add toggle mechanic + [\#3755](https://github.com/matrix-org/matrix-react-sdk/pull/3755) + * stop using ReactDOM.findDOMNode in componentWillUnmount, use refs + [\#3771](https://github.com/matrix-org/matrix-react-sdk/pull/3771) + * Add alt="" to presentational images + [\#3772](https://github.com/matrix-org/matrix-react-sdk/pull/3772) + * Fix room list filtering weird case sensitivity + [\#3759](https://github.com/matrix-org/matrix-react-sdk/pull/3759) + * Don't show the 'verify' button if the user is verified + [\#3758](https://github.com/matrix-org/matrix-react-sdk/pull/3758) + * Switch to using checkDeviceTrust + [\#3757](https://github.com/matrix-org/matrix-react-sdk/pull/3757) + * Migrate away from React Legacy contexts API + [\#3743](https://github.com/matrix-org/matrix-react-sdk/pull/3743) + * Migrate key backups to SSSS + [\#3749](https://github.com/matrix-org/matrix-react-sdk/pull/3749) + * Get rid of stripped-emoji.json in favour of an in-memory single source of + truth + [\#3745](https://github.com/matrix-org/matrix-react-sdk/pull/3745) + * Combine cross signing and verification over DM feature flags + [\#3753](https://github.com/matrix-org/matrix-react-sdk/pull/3753) + * apply unhomoglyph when filtering room list to fuzzify it + [\#3754](https://github.com/matrix-org/matrix-react-sdk/pull/3754) + * Make EmojiPicker an unmanaged Context Menu as it is too complex to be + managed + [\#3746](https://github.com/matrix-org/matrix-react-sdk/pull/3746) + * Internationalise M_TOO_LARGE error from Synapse + [\#3750](https://github.com/matrix-org/matrix-react-sdk/pull/3750) + * Replace UserInfo avatar with for fallback logic + [\#3748](https://github.com/matrix-org/matrix-react-sdk/pull/3748) + * Dropdown stop keyboard propagation if key handled + [\#3741](https://github.com/matrix-org/matrix-react-sdk/pull/3741) + * Fix right panel for multiple member info viewings + [\#3742](https://github.com/matrix-org/matrix-react-sdk/pull/3742) + * Fix Field validation tooltip sticking if blurred before async validation + resolved + [\#3740](https://github.com/matrix-org/matrix-react-sdk/pull/3740) + * Fix UserInfo exploding without a room being passed to it + [\#3738](https://github.com/matrix-org/matrix-react-sdk/pull/3738) + * Fix room directory maintaining and error state + [\#3737](https://github.com/matrix-org/matrix-react-sdk/pull/3737) + * Stop trapping tab in AddressPickerDialog + [\#3735](https://github.com/matrix-org/matrix-react-sdk/pull/3735) + * Stop using KeyboardEvent.keyCode as it is deprecated + [\#3736](https://github.com/matrix-org/matrix-react-sdk/pull/3736) + * Implement new design for uploading/removing avatars + [\#3733](https://github.com/matrix-org/matrix-react-sdk/pull/3733) + * Fix aspect ratio on room/profile avatar preview + [\#3731](https://github.com/matrix-org/matrix-react-sdk/pull/3731) + * Switch to react-focus-lock for it to comprehend Portals + [\#3732](https://github.com/matrix-org/matrix-react-sdk/pull/3732) + * Make combobox dropdown keyboard and screen reader accessible + [\#3729](https://github.com/matrix-org/matrix-react-sdk/pull/3729) + * Verify users when cross-signing enabled + [\#3728](https://github.com/matrix-org/matrix-react-sdk/pull/3728) + * Update from Weblate + [\#3730](https://github.com/matrix-org/matrix-react-sdk/pull/3730) + * Improve a11y of the unignore button in Settings + [\#3727](https://github.com/matrix-org/matrix-react-sdk/pull/3727) + * Fix ToggleSwitch A11Y (trapping tab and switch v. checkbox) + [\#3726](https://github.com/matrix-org/matrix-react-sdk/pull/3726) + * Make URL previews dismissable via keyboard and accessible to screen readers + [\#3725](https://github.com/matrix-org/matrix-react-sdk/pull/3725) + * Create new key backups using secret storage + [\#3720](https://github.com/matrix-org/matrix-react-sdk/pull/3720) + * Replace sign-ins with sessions + [\#3721](https://github.com/matrix-org/matrix-react-sdk/pull/3721) + * Refactor RightPanel to match expected behaviour + [\#3703](https://github.com/matrix-org/matrix-react-sdk/pull/3703) + * Render policy room event updates in the timeline + [\#3716](https://github.com/matrix-org/matrix-react-sdk/pull/3716) + * Wrap the await call for unknown device lookups + [\#3718](https://github.com/matrix-org/matrix-react-sdk/pull/3718) + * Add testing flow to bootstrap secret storage + [\#3640](https://github.com/matrix-org/matrix-react-sdk/pull/3640) + * Fix remaining context menu regressions + [\#3715](https://github.com/matrix-org/matrix-react-sdk/pull/3715) + * Migrate away from React Legacy string refs + [\#3712](https://github.com/matrix-org/matrix-react-sdk/pull/3712) + * Update copy for DM invites + [\#3706](https://github.com/matrix-org/matrix-react-sdk/pull/3706) + * Fix message action bar reaction picker regression + [\#3714](https://github.com/matrix-org/matrix-react-sdk/pull/3714) + * Add what-input to allow different scoping to focus-visible for MAB a11y + [\#3709](https://github.com/matrix-org/matrix-react-sdk/pull/3709) + * Mark the This/All Rooms scope buttons as radios for a11y + [\#3708](https://github.com/matrix-org/matrix-react-sdk/pull/3708) + * Switch ReactionsRowButton to an AccessibleButton for space/enter handling + [\#3707](https://github.com/matrix-org/matrix-react-sdk/pull/3707) + * Change the (edited) link to an AccessibleButton for a11y + [\#3710](https://github.com/matrix-org/matrix-react-sdk/pull/3710) + * Update from Weblate + [\#3713](https://github.com/matrix-org/matrix-react-sdk/pull/3713) + * Fix ?via= args in SpecPermalinkConstructor.js + [\#3694](https://github.com/matrix-org/matrix-react-sdk/pull/3694) + * Don't mark a room as unread when server ACLs are set + [\#3705](https://github.com/matrix-org/matrix-react-sdk/pull/3705) + * Make reaction buttons more accessible + [\#3704](https://github.com/matrix-org/matrix-react-sdk/pull/3704) + * yarn upgrade + [\#3701](https://github.com/matrix-org/matrix-react-sdk/pull/3701) + * Make CI scripts executable + [\#3698](https://github.com/matrix-org/matrix-react-sdk/pull/3698) + * ARIA compliant context menus + [\#3611](https://github.com/matrix-org/matrix-react-sdk/pull/3611) + Changes in [1.7.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.5) (2019-12-09) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.5-rc.1...v1.7.5) diff --git a/README.md b/README.md index c2e3737b81..0fbed22030 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold: * After creating a new component you must run `yarn reskindex` to regenerate the `component-index.js` for the SDK (used in future for skinning) + * The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css). CSS for matrix-react-sdk currently resides in diff --git a/__mocks__/browser-request.js b/__mocks__/browser-request.js new file mode 100644 index 0000000000..7d231fb9db --- /dev/null +++ b/__mocks__/browser-request.js @@ -0,0 +1,17 @@ +const en = require("../src/i18n/strings/en_EN"); + +module.exports = jest.fn((opts, cb) => { + const url = opts.url || opts.uri; + if (url && url.endsWith("languages.json")) { + cb(undefined, {status: 200}, JSON.stringify({ + "en": { + "fileName": "en_EN.json", + "label": "English", + }, + })); + } else if (url && url.endsWith("en_EN.json")) { + cb(undefined, {status: 200}, JSON.stringify(en)); + } else { + cb(true, {status: 404}, ""); + } +}); diff --git a/__mocks__/imageMock.js b/__mocks__/imageMock.js new file mode 100644 index 0000000000..474ac702b4 --- /dev/null +++ b/__mocks__/imageMock.js @@ -0,0 +1 @@ +module.exports = "image-file-stub"; diff --git a/__mocks__/languages.json b/__mocks__/languages.json new file mode 100644 index 0000000000..f62fe9b9b4 --- /dev/null +++ b/__mocks__/languages.json @@ -0,0 +1,10 @@ +{ + "en": { + "fileName": "en_EN.json", + "label": "English" + }, + "en-us": { + "fileName": "en_US.json", + "label": "English (US)" + } +} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000000..c83be72518 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,26 @@ +module.exports = { + "sourceMaps": "inline", + "presets": [ + ["@babel/preset-env", { + "targets": { + "browsers": [ + "last 2 versions" + ] + }, + "modules": "commonjs" + }], + "@babel/preset-typescript", + "@babel/preset-flow", + "@babel/preset-react" + ], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + "@babel/plugin-proposal-export-default-from", + "@babel/plugin-proposal-numeric-separator", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-object-rest-spread", + "@babel/plugin-transform-flow-comments", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-transform-runtime" + ] +}; diff --git a/docs/skinning.md b/docs/skinning.md new file mode 100644 index 0000000000..229bc78372 --- /dev/null +++ b/docs/skinning.md @@ -0,0 +1,71 @@ +# Skinning + +The react-sdk can be skinned to replace presentation components, CSS, or +other relevant parts of the SDK. Typically consumers will replace entire +components and get the ability for custom CSS as a result. + +This doc isn't exhaustive on how skinning works, though it should cover +some of the more complicated parts such as component replacement. + +## Loading a skin + +1. Generate a `component-index.js` (preferably using the tools that the react-sdk +exposes). This can typically be done with a npm script like `"reskindex -h src/header"`. +2. In your app's entry point, add something like this code: + ```javascript + import {loadSkin} from "matrix-react-sdk"; + loadSkin(import("component-index").components); + // The rest of your imports go under this. + ``` +3. Import the remainder of the SDK and bootstrap your app. + +It is extremely important that you **do not** import anything else from the +SDK prior to loading your skin as otherwise the skin might not work. Loading +the skin should be one of the first things your app does, if not the very +first thing. + +Additionally, **do not** provide `loadSkin` with the react-sdk components +themselves otherwise the app might explode. The SDK is already aware of its +components and doesn't need to be told. + +## Replacing components + +Components that replace the react-sdk ones MUST have a `replaces` static +key on the component's class to describe which component it overrides. For +example, if your `VectorAuthPage` component is meant to replace the react-sdk +`AuthPage` component then you'd add `static replaces = 'views.auth.AuthPage';` +to the `VectorAuthPage` class. + +Other than that, the skin just needs to be loaded normally as mentioned above. +Consumers of the SDK likely will not be interested in the rest of this section. + +### SDK developer notes + +Components in the react-sdk MUST be decorated with the `@replaceableComponent` +function. For components that can't use the decorator, they must use a +variation that provides similar functionality. The decorator gives consumers +an opportunity to load skinned components by abusing import ordering and +behaviour. + +Decorators are executed at import time which is why we can abuse the import +ordering behaviour: importing `loadSkin` doesn't trigger any components to +be imported, allowing the consumer to specify a skin. When the consumer does +import a component (for example, `MatrixChat`), it starts to pull in all the +components via `import` statements. When the components get pulled in the +decorator checks with the skinned components to see if it should be replacing +the component being imported. The decorator then effectively replaces the +components when needed by specifying the skinned component as an override for +the SDK's component, which should in theory override critical functions like +`render()` and lifecycle event handlers. + +The decorator also means that older usage of `getComponent()` is no longer +required because components should be replaced by the decorator. Eventually +the react-sdk should only have one usage of `getComponent()`: the decorator. + +The decorator assumes that if `getComponent()` returns null that there is +no skinned version of the component and continues on using the SDK's component. +In previous versions of the SDK, the function would throw an error instead +because it also expected the skin to list the SDK's components as well, however +that is no longer possible due to the above. + +In short, components should always be `import`ed. diff --git a/jenkins.sh b/jenkins.sh deleted file mode 100755 index 70bc12e42d..0000000000 --- a/jenkins.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -set -e - -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -nvm use 10 - -set -x - -scripts/fetchdep.sh matrix-org matrix-js-sdk - -pushd matrix-js-sdk -yarn link -yarn install -popd - -yarn link matrix-js-sdk - -# install the other dependencies -yarn install - -# run the mocha tests -yarn test --no-colors - -# run eslint -yarn lintall -f checkstyle -o eslint.xml || true - -# re-run the linter, excluding any files known to have errors or warnings. -yarn lintwithexclusions - -# lint styles -yarn stylelint - -# delete the old tarball, if it exists -rm -f matrix-react-sdk-*.tgz - -# build our tarball -yarn pack diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index d55be049bb..0000000000 --- a/karma.conf.js +++ /dev/null @@ -1,228 +0,0 @@ -// karma.conf.js - the config file for karma, which runs our tests. - -var path = require('path'); -var fs = require('fs'); - -/* - * We use webpack to build our tests. It's a pain to have to wait for webpack - * to build everything; however it's the easiest way to load our dependencies - * from node_modules. - * - * If you run karma in multi-run mode (with `yarn test-multi`), it will watch - * the tests for changes, and webpack will rebuild using a cache. This is much quicker - * than a clean rebuild. - */ - -// the name of the test file. By default, a special file which runs all tests. -// -// TODO: this could be a pattern, and karma would run each file, with a -// separate webpack bundle for each file. But then we get a separate instance -// of the sdk, and each of the dependencies, for each test file, and everything -// gets very confused. Can we persuade webpack to put all of the dependencies -// in a 'common' bundle? -// -var testFile = process.env.KARMA_TEST_FILE || 'test/all-tests.js'; - - -process.env.PHANTOMJS_BIN = 'node_modules/.bin/phantomjs'; - -function fileExists(name) { - try { - fs.statSync(name); - return true; - } catch (e) { - return false; - } -} - -// try find the gemini-scrollbar css in an version-agnostic way -var gsCss = 'node_modules/gemini-scrollbar/gemini-scrollbar.css'; -if (!fileExists(gsCss)) { - gsCss = 'node_modules/react-gemini-scrollbar/'+gsCss; -} - - -module.exports = function (config) { - config.set({ - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'], - - // list of files / patterns to load in the browser - files: [ - testFile, - gsCss, - - // some images to reduce noise from the tests - {pattern: 'test/img/*', watched: false, included: false, - served: true, nocache: false}, - // translation files - {pattern: 'src/i18n/strings/*', watcheed: false, included: false, served: true}, - {pattern: 'test/i18n/*', watched: false, included: false, served: true}, - ], - - proxies: { - // redirect img links to the karma server - "/img/": "/base/test/img/", - // special languages.json file for the tests - "/i18n/languages.json": "/base/test/i18n/languages.json", - // and redirect i18n requests - "/i18n/": "/base/src/i18n/strings/", - }, - - // list of files to exclude - // - // This doesn't work. It turns out that it's webpack which does the - // watching of the /test directory (karma only watches `testFile` - // itself). Webpack watches the directory so that it can spot - // new tests, which is fair enough; unfortunately it triggers a rebuild - // every time a lockfile is created in that directory, and there - // doesn't seem to be any way to tell webpack to ignore particular - // files in a watched directory. - // - // exclude: [ - // '**/.#*' - // ], - - // preprocess matching files before serving them to the browser - // available preprocessors: - // https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - 'test/**/*.js': ['webpack', 'sourcemap'] - }, - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['logcapture', 'spec', 'summary'], - - specReporter: { - suppressErrorSummary: false, // do print error summary - suppressFailed: false, // do print information about failed tests - suppressPassed: false, // do print information about passed tests - showSpecTiming: true, // print the time elapsed for each spec - }, - - client: { - captureLogs: true, - }, - - // web server port - port: 9876, - - // enable / disable colors in the output (reporters and logs) - colors: true, - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || - // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - // - // This is strictly for logs that would be generated by the browser itself and we - // don't want to log about missing images, which are emitted on LOG_WARN. - logLevel: config.LOG_ERROR, - - // enable / disable watching file and executing tests whenever any file - // changes - autoWatch: true, - - // start these browsers - // available browser launchers: - // https://npmjs.org/browse/keyword/karma-launcher - browsers: [ - 'Chrome', - //'PhantomJS', - //'ChromeHeadless', - ], - - customLaunchers: { - 'VectorChromeHeadless': { - base: 'Chrome', - flags: [ - '--no-sandbox', - // See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md - '--headless', - '--disable-gpu', - // Without a remote debugging port, Google Chrome exits immediately. - '--remote-debugging-port=9222', - ], - } - }, - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - // singleRun: false, - - // Concurrency level - // how many browser should be started simultaneous - concurrency: Infinity, - - webpack: { - module: { - rules: [ - { - test: /\.js$/, loader: "babel-loader", - include: [path.resolve('./src'), - path.resolve('./test'), - ] - }, - { - test: /\.(gif|png|svg|ttf|woff2)$/, - loader: 'file-loader', - }, - ], - noParse: [ - // for cross platform compatibility use [\\\/] as the path separator - // this ensures that the regex trips on both Windows and *nix - - // don't parse the languages within highlight.js. They - // cause stack overflows - // (https://github.com/webpack/webpack/issues/1721), and - // there is no need for webpack to parse them - they can - // just be included as-is. - /highlight\.js[\\\/]lib[\\\/]languages/, - - // olm takes ages for webpack to process, and it's already heavily - // optimised, so there is little to gain by us uglifying it. - /olm[\\\/](javascript[\\\/])?olm\.js$/, - - // also disable parsing for sinon, because it - // tries to do voodoo with 'require' which upsets - // webpack (https://github.com/webpack/webpack/issues/304) - /sinon[\\\/]pkg[\\\/]sinon\.js$/, - ], - }, - resolve: { - alias: { - // alias any requires to the react module to the one in our - // path, otherwise we tend to get the react source included - // twice when using `npm link` / `yarn link`. - react: path.resolve('./node_modules/react'), - - 'matrix-react-sdk': path.resolve('test/skinned-sdk.js'), - 'sinon': 'sinon/pkg/sinon.js', - }, - modules: [ - path.resolve('./test'), - "node_modules" - ], - }, - devtool: 'inline-source-map', - externals: { - // Don't try to bundle electron: leave it as a commonjs dependency - // (the 'commonjs' here means it will output a 'require') - "electron": "commonjs electron", - }, - // make sure we're flagged as development to avoid wasting time optimising - mode: 'development', - }, - - webpackMiddleware: { - stats: { - // don't fill the console up with a mahoosive list of modules - chunks: false, - }, - }, - - browserNoActivityTimeout: 15000, - }); -}; diff --git a/package.json b/package.json index 7ef14e6635..16e7f943f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.5", + "version": "1.7.6", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -8,57 +8,51 @@ "url": "https://github.com/matrix-org/matrix-react-sdk" }, "license": "Apache-2.0", - "main": "lib/index.js", "files": [ - ".babelrc", - ".eslintrc.js", + "lib", + "res", + "src", + "scripts", + "git-revision.txt", + "docs", + "header", "CHANGELOG.md", "CONTRIBUTING.rst", "LICENSE", "README.md", - "code_style.md", - "git-revision.txt", - "header", - "jenkins.sh", - "karma.conf.js", - "lib", - "package.json", - "release.sh", - "scripts", - "src", - "test", - "res" + "package.json" ], "bin": { "reskindex": "scripts/reskindex.js", "matrix-gen-i18n": "scripts/gen-i18n.js", "matrix-prune-i18n": "scripts/prune-i18n.js" }, + "main": "./lib/index.js", + "typings": "./lib/index.d.ts", + "matrix_src_main": "./src/index.js", "scripts": { - "reskindex": "node scripts/reskindex.js -h header", - "reskindex:watch": "node scripts/reskindex.js -h header -w", - "rethemendex": "res/css/rethemendex.sh", "i18n": "matrix-gen-i18n", "prunei18n": "matrix-prune-i18n", "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", - "build": "yarn reskindex && yarn start:init", - "build:watch": "babel src -w --skip-initial-build -d lib --source-maps --copy-files", - "start": "yarn start:init && yarn start:all", - "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn build:watch\" \"yarn reskindex:watch\"", - "start:init": "babel src -d lib --source-maps --copy-files", - "lint": "eslint src/", - "lintall": "eslint src/ test/", - "lintwithexclusions": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", - "stylelint": "stylelint 'res/css/**/*.scss'", + "reskindex": "node scripts/reskindex.js -h header", + "reskindex:watch": "node scripts/reskindex.js -h header -w", + "rethemendex": "res/css/rethemendex.sh", "clean": "rimraf lib", - "prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt", - "test": "karma start --single-run=true --browsers VectorChromeHeadless", - "test-multi": "karma start", - "e2etests": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080" + "build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", + "build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js\" src", + "build:types": "tsc --emitDeclarationOnly", + "start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all", + "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", + "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", + "lint": "yarn lint:types && yarn lint:ts && yarn lint:js && yarn lint:style", + "lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", + "lint:ts": "tslint --project ./tsconfig.json -t stylish", + "lint:types": "tsc --noEmit", + "lint:style": "stylelint 'res/css/**/*.scss'", + "test": "jest", + "test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080" }, "dependencies": { - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-runtime": "^6.26.0", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", @@ -74,7 +68,6 @@ "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", - "react-focus-lock": "^2.2.1", "focus-visible": "^5.0.2", "fuse.js": "^2.2.0", "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", @@ -82,12 +75,13 @@ "glob": "^5.0.14", "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", + "html-entities": "^1.2.1", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "3.0.0", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", @@ -99,13 +93,10 @@ "react-addons-css-transition-group": "15.6.2", "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.9.0", + "react-focus-lock": "^2.2.1", "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", - "slate": "^0.41.2", - "slate-html-serializer": "^0.6.1", - "slate-md-serializer": "github:matrix-org/slate-md-serializer#f7c4ad3", - "slate-react": "^0.18.10", "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", "velocity-animate": "^1.5.2", @@ -114,22 +105,28 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { - "babel-cli": "^6.26.0", - "babel-core": "^6.26.3", - "babel-eslint": "^10.0.1", - "babel-loader": "^7.1.5", - "babel-plugin-add-module-exports": "^0.2.1", - "babel-plugin-transform-builtin-extend": "^1.1.2", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-plugin-transform-runtime": "^6.23.0", - "babel-polyfill": "^6.26.0", - "babel-preset-es2015": "^6.24.1", - "babel-preset-es2016": "^6.24.1", - "babel-preset-es2017": "^6.24.1", - "babel-preset-react": "^6.24.1", + "@babel/cli": "^7.7.5", + "@babel/core": "^7.7.5", + "@babel/plugin-proposal-class-properties": "^7.7.4", + "@babel/plugin-proposal-decorators": "^7.7.4", + "@babel/plugin-proposal-export-default-from": "^7.7.4", + "@babel/plugin-proposal-numeric-separator": "^7.7.4", + "@babel/plugin-proposal-object-rest-spread": "^7.7.4", + "@babel/plugin-transform-flow-comments": "^7.7.4", + "@babel/plugin-transform-runtime": "^7.7.6", + "@babel/preset-env": "^7.7.6", + "@babel/preset-flow": "^7.7.4", + "@babel/preset-react": "^7.7.4", + "@babel/preset-typescript": "^7.7.4", + "@babel/register": "^7.7.4", + "@babel/runtime": "^7.7.6", + "@peculiar/webcrypto": "^1.0.22", + "babel-eslint": "^10.0.3", + "babel-jest": "^24.9.0", "chokidar": "^2.1.2", "concurrently": "^4.0.1", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.15.1", "eslint": "^5.12.0", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", @@ -138,32 +135,35 @@ "eslint-plugin-react": "^7.7.0", "eslint-plugin-react-hooks": "^2.0.1", "estree-walker": "^0.5.0", - "expect": "^24.1.0", "file-loader": "^3.0.1", "flow-parser": "^0.57.3", - "jest-mock": "^23.2.0", - "karma": "^4.0.1", - "karma-chrome-launcher": "^2.2.0", - "karma-cli": "^1.0.1", - "karma-logcapture-reporter": "0.0.1", - "karma-mocha": "^1.3.0", - "karma-sourcemap-loader": "^0.3.7", - "karma-spec-reporter": "^0.0.31", - "karma-summary-reporter": "^1.5.1", - "karma-webpack": "^4.0.0-beta.0", + "jest": "^24.9.0", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", - "mocha": "^5.0.5", "react-test-renderer": "^16.9.0", "require-json": "0.0.1", "rimraf": "^2.4.3", - "sinon": "^5.0.7", "source-map-loader": "^0.2.3", "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", "stylelint-scss": "^3.9.0", + "tslint": "^5.20.1", + "typescript": "^3.7.3", "walk": "^2.3.9", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" + }, + "jest": { + "testMatch": [ + "/test/**/*-test.js" + ], + "setupTestFrameworkScriptFile": "/test/setupTests.js", + "moduleNameMapper": { + "\\.(gif|png|svg|ttf|woff2)$": "/__mocks__/imageMock.js", + "\\$webapp/i18n/languages.json": "/__mocks__/languages.json" + }, + "transformIgnorePatterns": [ + "/node_modules/(?!matrix-js-sdk).+$" + ] } } diff --git a/res/css/_components.scss b/res/css/_components.scss index 7b8ca77739..a9a114a4cf 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -28,6 +28,7 @@ @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_ViewSource.scss"; +@import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; @import "./views/auth/_AuthBody.scss"; @import "./views/auth/_AuthButtons.scss"; @@ -56,6 +57,7 @@ @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; +@import "./views/dialogs/_DMInviteDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss new file mode 100644 index 0000000000..c258ce4ec7 --- /dev/null +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -0,0 +1,51 @@ +/* +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. +*/ + +.mx_CompleteSecurity_header { + display: flex; + align-items: center; +} + +.mx_CompleteSecurity_headerIcon { + width: 24px; + height: 24px; + margin: 0 4px; + position: relative; +} + +.mx_CompleteSecurity_heroIcon { + width: 128px; + height: 128px; + position: relative; + margin: 0 auto; +} + +.mx_CompleteSecurity_body { + font-size: 15px; +} + +.mx_CompleteSecurity_actionRow { + display: flex; + justify-content: flex-end; + + .mx_AccessibleButton { + margin-inline-start: 18px; + + &.warning { + color: $warning-color; + } + } +} diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss index 972f608caf..2c8d608950 100644 --- a/res/css/views/context_menus/_StatusMessageContextMenu.scss +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -61,5 +61,5 @@ input.mx_StatusMessageContextMenu_message { } .mx_StatusMessageContextMenu_actionContainer .mx_Spinner { - justify-content: start; + justify-content: flex-start; } diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss index d3a8f6ff42..7416ec2ef4 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.scss +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -30,7 +30,7 @@ limitations under the License. > div { display: flex; - align-items: start; + align-items: flex-start; margin: 5px 0; input[type=checkbox] { diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss new file mode 100644 index 0000000000..f806e85120 --- /dev/null +++ b/res/css/views/dialogs/_DMInviteDialog.scss @@ -0,0 +1,197 @@ +/* +Copyright 2019, 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. +*/ + +.mx_DMInviteDialog_addressBar { + display: flex; + flex-direction: row; + + .mx_DMInviteDialog_editor { + flex: 1; + width: 100%; // Needed to make the Field inside grow + background-color: $user-tile-hover-bg-color; + border-radius: 4px; + min-height: 25px; + padding-left: 8px; + overflow-x: hidden; + overflow-y: auto; + + .mx_DMInviteDialog_userTile { + display: inline-block; + float: left; + position: relative; + top: 7px; + } + + // Using a textarea for this element, to circumvent autofill + // Mostly copied from AddressPickerDialog + textarea, + textarea:focus { + height: 34px; + line-height: 34px; + font-size: 14px; + padding-left: 12px; + margin: 0 !important; + border: 0 !important; + outline: 0 !important; + resize: none; + overflow: hidden; + box-sizing: border-box; + word-wrap: nowrap; + + // Roughly fill about 2/5ths of the available space. This is to try and 'fill' the + // remaining space after a bunch of pills, but is a bit hacky. Ideally we'd have + // support for "fill remaining width", but traditional tricks don't work with what + // we're pushing into this "field". Flexbox just makes things worse. The theory is + // that users won't need more than about 2/5ths of the input to find the person + // they're looking for. + width: 40%; + } + } + + .mx_DMInviteDialog_goButton { + width: 48px; + margin-left: 10px; + height: 25px; + line-height: 25px; + } +} + +.mx_DMInviteDialog_section { + padding-bottom: 10px; + + h3 { + font-size: 12px; + color: $muted-fg-color; + font-weight: bold; + text-transform: uppercase; + } +} + +.mx_DMInviteDialog_roomTile { + cursor: pointer; + padding: 5px 10px; + + &:hover { + background-color: $user-tile-hover-bg-color; + border-radius: 4px; + } + + * { + vertical-align: middle; + } + + .mx_DMInviteDialog_roomTile_avatarStack { + display: inline-block; + position: relative; + width: 36px; + height: 36px; + + & > * { + position: absolute; + top: 0; + left: 0; + } + } + + .mx_DMInviteDialog_roomTile_selected { + width: 36px; + height: 36px; + border-radius: 36px; + background-color: $username-variant1-color; + display: inline-block; + position: relative; + + &::before { + content: ""; + width: 24px; + height: 24px; + grid-column: 1; + grid-row: 1; + mask-image: url('$(res)/img/feather-customised/check.svg'); + mask-size: 100%; + mask-repeat: no-repeat; + position: absolute; + top: 6px; // 50% + left: 6px; // 50% + background-color: #ffffff; // this is fine without a var because it's for both themes + } + } + + .mx_DMInviteDialog_roomTile_name { + font-weight: 600; + font-size: 14px; + color: $primary-fg-color; + margin-left: 7px; + } + + .mx_DMInviteDialog_roomTile_userId { + font-size: 12px; + color: $muted-fg-color; + margin-left: 7px; + } + + .mx_DMInviteDialog_roomTile_time { + text-align: right; + font-size: 12px; + color: $muted-fg-color; + float: right; + line-height: 36px; // Height of the avatar to keep the time vertically aligned + } + + .mx_DMInviteDialog_roomTile_highlight { + font-weight: 900; + } +} + +// Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog. +.mx_DMInviteDialog_userTile { + margin-right: 8px; + + .mx_DMInviteDialog_userTile_pill { + background-color: $username-variant1-color; + border-radius: 12px; + display: inline-block; + height: 24px; + line-height: 24px; + padding-left: 8px; + padding-right: 8px; + color: #ffffff; // this is fine without a var because it's for both themes + + .mx_DMInviteDialog_userTile_avatar { + border-radius: 20px; + position: relative; + left: -5px; + top: 2px; + } + + img.mx_DMInviteDialog_userTile_avatar { + vertical-align: top; + } + + .mx_DMInviteDialog_userTile_name { + vertical-align: top; + } + + .mx_DMInviteDialog_userTile_threepidAvatar { + background-color: #ffffff; // this is fine without a var because it's for both themes + } + } + + .mx_DMInviteDialog_userTile_remove { + display: inline-block; + margin-left: 4px; + } +} diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index 08839d8493..aa66e97f9e 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.scss +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -29,6 +29,11 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/users-sm.svg'); } +.mx_RoomSettingsDialog_bridgesIcon::before { + // This icon is pants, please improve :) + mask-image: url('$(res)/img/feather-customised/bridge.svg'); +} + .mx_RoomSettingsDialog_warningIcon::before { mask-image: url('$(res)/img/feather-customised/warning-triangle.svg'); } @@ -50,3 +55,17 @@ limitations under the License. mask-size: 36px; mask-position: center; } + +.mx_RoomSettingsDialog_BridgeList { + padding: 0; +} + +.mx_RoomSettingsDialog_BridgeList li { + list-style-type: none; + padding: 5px; + margin-bottom: 5px; + border-width: 1px 0px; + border-color: #dee1f3; + border-style: solid; +} + diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 7ba5f01a76..04ee575867 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -32,7 +32,7 @@ limitations under the License. .mx_CreateKeyBackupDialog_passPhraseContainer { display: flex; - align-items: start; + align-items: flex-start; } .mx_CreateKeyBackupDialog_passPhraseHelp { diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 757d8028f0..5899abdf73 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -33,7 +33,7 @@ limitations under the License. .mx_CreateSecretStorageDialog_passPhraseContainer { display: flex; - align-items: start; + align-items: flex-start; } .mx_CreateSecretStorageDialog_passPhraseHelp { diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss index e73e6c58f1..15b4832dc5 100644 --- a/res/css/views/rooms/_MemberDeviceInfo.scss +++ b/res/css/views/rooms/_MemberDeviceInfo.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_MemberDeviceInfo { display: flex; padding-bottom: 10px; - align-items: start; + align-items: flex-start; } .mx_MemberDeviceInfo_icon { diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 12e45a07c9..5efca51844 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -101,7 +101,7 @@ limitations under the License. display: flex; flex-direction: column; min-height: 60px; - justify-content: start; + justify-content: flex-start; align-items: flex-start; font-size: 14px; margin-right: 6px; diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index f1e4456cc1..45b9733faa 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -263,3 +263,24 @@ limitations under the License. .mx_RoomHeader_pinsIndicatorUnread { background-color: $pinned-unread-color; } + +.mx_RoomHeader_PrivateIcon.mx_RoomHeader_isPrivate { + width: 12px; + height: 12px; + position: relative; + display: block !important; + + &::before { + background-color: $roomtile-name-color; + mask-image: url('$(res)/img/feather-customised/lock-solid.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss index 68e2bf861e..85d42ca4b4 100644 --- a/res/css/views/rooms/_RoomRecoveryReminder.scss +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -40,4 +40,5 @@ limitations under the License. .mx_RoomRecoveryReminder_secondary { font-size: 90%; + margin-top: 1em; } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index e5c7948216..cb1137bb2f 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -200,3 +200,31 @@ limitations under the License. .mx_GroupInviteTile .mx_RoomTile_name { flex: 1; } + +.mx_RoomTile.mx_RoomTile.mx_RoomTile_isPrivate .mx_RoomTile_name { + // Scoot the padding in a bit from 6px to make it look better + padding-left: 3px; +} + +.mx_RoomTile.mx_RoomTile_isPrivate .mx_RoomTile_PrivateIcon { + width: 12px; + height: 12px; + position: relative; + display: block !important; + // Align the padlock with unencrypted room names + margin-left: 6px; + + &::before { + background-color: $roomtile-name-color; + mask-image: url('$(res)/img/feather-customised/lock-solid.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} diff --git a/res/img/feather-customised/bridge.svg b/res/img/feather-customised/bridge.svg new file mode 100644 index 0000000000..f8f3468155 --- /dev/null +++ b/res/img/feather-customised/bridge.svg @@ -0,0 +1,50 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/res/img/feather-customised/lock-solid.svg b/res/img/feather-customised/lock-solid.svg new file mode 100644 index 0000000000..9eb8b6a4c5 --- /dev/null +++ b/res/img/feather-customised/lock-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/icon-email-pill-avatar.svg b/res/img/icon-email-pill-avatar.svg new file mode 100644 index 0000000000..6b0ac200a5 --- /dev/null +++ b/res/img/icon-email-pill-avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/icon-pill-remove.svg b/res/img/icon-pill-remove.svg new file mode 100644 index 0000000000..adf6fd4771 --- /dev/null +++ b/res/img/icon-pill-remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index eadde4c672..a3515a9d99 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -16,6 +16,7 @@ $room-highlight-color: #343a46; // typical text (dark-on-white in light skin) $primary-fg-color: $text-primary-color; $primary-bg-color: $bg-color; +$muted-fg-color: $header-panel-text-primary-color; // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -172,6 +173,8 @@ $interactive-tooltip-fg-color: #ffffff; $breadcrumb-placeholder-bg-color: #272c35; +$user-tile-hover-bg-color: $header-panel-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { @@ -243,3 +246,13 @@ $breadcrumb-placeholder-bg-color: #272c35; } } } + +// diff highlight colors +// intentionally swapped to avoid inversion +.hljs-addition { + background: #fdd; +} + +.hljs-deletion { + background: #dfd; +} diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 0a3ef812b8..288fb3cadc 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -21,6 +21,7 @@ $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) $primary-fg-color: #2e2f32; $primary-bg-color: #ffffff; +$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text // used for dialog box text $light-fg-color: #747474; @@ -293,6 +294,8 @@ $interactive-tooltip-fg-color: #ffffff; $breadcrumb-placeholder-bg-color: #e8eef5; +$user-tile-hover-bg-color: $header-panel-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { @@ -338,3 +341,12 @@ $breadcrumb-placeholder-bg-color: #e8eef5; color: $accent-color; text-decoration: none; } + +// diff highlight colors +.hljs-addition { + background: #dfd; +} + +.hljs-deletion { + background: #fdd; +} diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index ae88ef70c7..a592888292 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -36,7 +36,8 @@ echo "--- Install synapse & other dependencies" ./install.sh # install static webserver to server symlinked local copy of riot ./riot/install-webserver.sh -mkdir logs || rm -r logs/* +rm -r logs || true +mkdir logs echo "+++ Running end-to-end tests" TESTS_STARTED=1 ./run.sh --no-sandbox --log-directory logs/ diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh index 6484ebab29..a2e2e59a45 100755 --- a/scripts/ci/install-deps.sh +++ b/scripts/ci/install-deps.sh @@ -7,6 +7,7 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk pushd matrix-js-sdk yarn link yarn install +yarn build popd yarn link matrix-js-sdk diff --git a/scripts/ci/unit-tests.sh b/scripts/ci/unit-tests.sh deleted file mode 100755 index 5b86190963..0000000000 --- a/scripts/ci/unit-tests.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones riot-web develop and runs the tests against our version of react-sdk. - -set -ev - -scripts/ci/build.sh -yarn test diff --git a/scripts/reskindex.js b/scripts/reskindex.js index 833151a298..3919295078 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -19,7 +19,6 @@ function reskindex() { prevFiles = files; var header = args.h || args.header; - var packageJson = JSON.parse(fs.readFileSync('./package.json')); var strm = fs.createWriteStream(componentIndexTmp); @@ -34,19 +33,7 @@ function reskindex() { strm.write(" * so you'd just be trying to swim upstream like a salmon.\n"); strm.write(" * You are not a salmon.\n"); strm.write(" */\n\n"); - - if (packageJson['matrix-react-parent']) { - const parentIndex = packageJson['matrix-react-parent'] + - '/lib/component-index'; - strm.write( -`let components = require('${parentIndex}').components; -if (!components) { - throw new Error("'${parentIndex}' didn't export components"); -} -`); - } else { - strm.write("let components = {};\n"); - } + strm.write("let components = {};\n"); for (var i = 0; i < files.length; ++i) { var file = files[i].replace('.js', ''); diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 694c2e124c..7a3250d0ca 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -16,8 +16,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from './MatrixClientPeg'; -import sdk from './index'; +import {MatrixClientPeg} from './MatrixClientPeg'; +import * as sdk from './index'; import Modal from './Modal'; import { _t } from './languageHandler'; import IdentityAuthClient from './IdentityAuthClient'; diff --git a/src/Analytics.js b/src/Analytics.js index 3e208ad6bd..d0c7a52814 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -18,7 +18,7 @@ import { getCurrentLanguage, _t, _td } from './languageHandler'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; import Modal from './Modal'; -import sdk from './index'; +import * as sdk from './index'; const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/; const hashVarRegex = /#\/(group|room|user)\/.*$/; @@ -306,4 +306,4 @@ class Analytics { if (!global.mxAnalytics) { global.mxAnalytics = new Analytics(); } -module.exports = global.mxAnalytics; +export default global.mxAnalytics; diff --git a/src/Avatar.js b/src/Avatar.js index 17860698cb..5a330c31e9 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -15,13 +15,14 @@ limitations under the License. */ 'use strict'; -import {ContentRepo} from 'matrix-js-sdk'; -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import DMRoomMap from './utils/DMRoomMap'; +import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; -module.exports = { - avatarUrlForMember: function(member, width, height, resizeMethod) { - let url = member.getAvatarUrl( +export function avatarUrlForMember(member, width, height, resizeMethod) { + let url; + if (member && member.getAvatarUrl) { + url = member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), Math.floor(width * window.devicePixelRatio), Math.floor(height * window.devicePixelRatio), @@ -29,106 +30,106 @@ module.exports = { false, false, ); - if (!url) { - // member can be null here currently since on invites, the JS SDK - // does not have enough info to build a RoomMember object for - // the inviter. - url = this.defaultAvatarUrlForString(member ? member.userId : ''); + } + if (!url) { + // member can be null here currently since on invites, the JS SDK + // does not have enough info to build a RoomMember object for + // the inviter. + url = defaultAvatarUrlForString(member ? member.userId : ''); + } + return url; +} + +export function avatarUrlForUser(user, width, height, resizeMethod) { + const url = getHttpUriForMxc( + MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, + Math.floor(width * window.devicePixelRatio), + Math.floor(height * window.devicePixelRatio), + resizeMethod, + ); + if (!url || url.length === 0) { + return null; + } + return url; +} + +export function defaultAvatarUrlForString(s) { + const images = ['03b381', '368bd6', 'ac3ba8']; + let total = 0; + for (let i = 0; i < s.length; ++i) { + total += s.charCodeAt(i); + } + return require('../res/img/' + images[total % images.length] + '.png'); +} + +/** + * returns the first (non-sigil) character of 'name', + * converted to uppercase + * @param {string} name + * @return {string} the first letter + */ +export function getInitialLetter(name) { + if (!name) { + // XXX: We should find out what causes the name to sometimes be falsy. + console.trace("`name` argument to `getInitialLetter` not supplied"); + return undefined; + } + if (name.length < 1) { + return undefined; + } + + let idx = 0; + const initial = name[0]; + if ((initial === '@' || initial === '#' || initial === '+') && name[1]) { + idx++; + } + + // string.codePointAt(0) would do this, but that isn't supported by + // some browsers (notably PhantomJS). + let chars = 1; + const first = name.charCodeAt(idx); + + // check if it’s the start of a surrogate pair + if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { + const second = name.charCodeAt(idx+1); + if (second >= 0xDC00 && second <= 0xDFFF) { + chars++; } - return url; - }, + } - avatarUrlForUser: function(user, width, height, resizeMethod) { - const url = ContentRepo.getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - ); - if (!url || url.length === 0) { - return null; - } - return url; - }, + const firstChar = name.substring(idx, idx+chars); + return firstChar.toUpperCase(); +} - defaultAvatarUrlForString: function(s) { - const images = ['03b381', '368bd6', 'ac3ba8']; - let total = 0; - for (let i = 0; i < s.length; ++i) { - total += s.charCodeAt(i); - } - return require('../res/img/' + images[total % images.length] + '.png'); - }, +export function avatarUrlForRoom(room, width, height, resizeMethod) { + const explicitRoomAvatar = room.getAvatarUrl( + MatrixClientPeg.get().getHomeserverUrl(), + width, + height, + resizeMethod, + false, + ); + if (explicitRoomAvatar) { + return explicitRoomAvatar; + } - /** - * returns the first (non-sigil) character of 'name', - * converted to uppercase - * @param {string} name - * @return {string} the first letter - */ - getInitialLetter(name) { - if (!name) { - // XXX: We should find out what causes the name to sometimes be falsy. - console.trace("`name` argument to `getInitialLetter` not supplied"); - return undefined; - } - if (name.length < 1) { - return undefined; - } - - let idx = 0; - const initial = name[0]; - if ((initial === '@' || initial === '#' || initial === '+') && name[1]) { - idx++; - } - - // string.codePointAt(0) would do this, but that isn't supported by - // some browsers (notably PhantomJS). - let chars = 1; - const first = name.charCodeAt(idx); - - // check if it’s the start of a surrogate pair - if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { - const second = name.charCodeAt(idx+1); - if (second >= 0xDC00 && second <= 0xDFFF) { - chars++; - } - } - - const firstChar = name.substring(idx, idx+chars); - return firstChar.toUpperCase(); - }, - - avatarUrlForRoom(room, width, height, resizeMethod) { - const explicitRoomAvatar = room.getAvatarUrl( + let otherMember = null; + const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); + if (otherUserId) { + otherMember = room.getMember(otherUserId); + } else { + // if the room is not marked as a 1:1, but only has max 2 members + // then still try to show any avatar (pref. other member) + otherMember = room.getAvatarFallbackMember(); + } + if (otherMember) { + return otherMember.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), width, height, resizeMethod, false, ); - if (explicitRoomAvatar) { - return explicitRoomAvatar; - } - - let otherMember = null; - const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); - if (otherUserId) { - otherMember = room.getMember(otherUserId); - } else { - // if the room is not marked as a 1:1, but only has max 2 members - // then still try to show any avatar (pref. other member) - otherMember = room.getAvatarFallbackMember(); - } - if (otherMember) { - return otherMember.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - width, - height, - resizeMethod, - false, - ); - } - return null; - }, -}; + } + return null; +} diff --git a/src/CallHandler.js b/src/CallHandler.js index ecbf6c2c12..33e15d3cc9 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -53,10 +53,10 @@ limitations under the License. * } */ -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; -import sdk from './index'; +import * as sdk from './index'; import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; import dis from './dispatcher'; @@ -302,7 +302,7 @@ function _onAction(payload) { switch (payload.action) { case 'place_call': { - if (module.exports.getAnyActiveCall()) { + if (callHandler.getAnyActiveCall()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { title: _t('Existing Call'), @@ -355,7 +355,7 @@ function _onAction(payload) { break; case 'incoming_call': { - if (module.exports.getAnyActiveCall()) { + if (callHandler.getAnyActiveCall()) { // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup. // we avoid rejecting with "busy" in case the user wants to answer it on a different device. // in future we could signal a "local busy" as a warning to the caller. @@ -523,7 +523,7 @@ if (!global.mxCallHandler) { const callHandler = { getCallForRoom: function(roomId) { - let call = module.exports.getCall(roomId); + let call = callHandler.getCall(roomId); if (call) return call; if (ConferenceHandler) { @@ -583,4 +583,4 @@ if (global.mxCallHandler === undefined) { global.mxCallHandler = callHandler; } -module.exports = global.mxCallHandler; +export default global.mxCallHandler; diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 0ce349f348..34379c029b 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -19,8 +19,8 @@ limitations under the License. import extend from './extend'; import dis from './dispatcher'; -import MatrixClientPeg from './MatrixClientPeg'; -import sdk from './index'; +import {MatrixClientPeg} from './MatrixClientPeg'; +import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import RoomViewStore from './stores/RoomViewStore'; diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index ab0a22e4d5..0773a8d32d 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -15,10 +15,10 @@ limitations under the License. */ import Modal from './Modal'; -import sdk from './index'; -import MatrixClientPeg from './MatrixClientPeg'; -import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase'; -import { decodeRecoveryKey } from 'matrix-js-sdk/lib/crypto/recoverykey'; +import * as sdk from './index'; +import {MatrixClientPeg} from './MatrixClientPeg'; +import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; +import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; // This stores the secret storage private keys in memory for the JS SDK. This is @@ -97,7 +97,7 @@ export const crossSigningCallbacks = { * * Additionally, the secret storage keys are cached during the scope of this function * to ensure the user is prompted only once for their secret storage - * passphrase. The cache is then + * passphrase. The cache is then cleared once the provided function completes. * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. diff --git a/src/Entities.js b/src/Entities.js index 8be1da0db8..872a837f3a 100644 --- a/src/Entities.js +++ b/src/Entities.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import sdk from './index'; +import * as sdk from './index'; function isMatch(query, name, uid) { query = query.toLowerCase(); @@ -105,36 +105,33 @@ class UserEntity extends Entity { } } +export function newEntity(jsx, matchFn) { + const entity = new Entity(); + entity.getJsx = function() { + return jsx; + }; + entity.matches = matchFn; + return entity; +} -module.exports = { - newEntity: function(jsx, matchFn) { - const entity = new Entity(); - entity.getJsx = function() { - return jsx; - }; - entity.matches = matchFn; - return entity; - }, +/** + * @param {RoomMember[]} members + * @return {Entity[]} + */ +export function fromRoomMembers(members) { + return members.map(function(m) { + return new MemberEntity(m); + }); +} - /** - * @param {RoomMember[]} members - * @return {Entity[]} - */ - fromRoomMembers: function(members) { - return members.map(function(m) { - return new MemberEntity(m); - }); - }, - - /** - * @param {User[]} users - * @param {boolean} showInviteButton - * @param {Function} inviteFn Called with the user ID. - * @return {Entity[]} - */ - fromUsers: function(users, showInviteButton, inviteFn) { - return users.map(function(u) { - return new UserEntity(u, showInviteButton, inviteFn); - }); - }, -}; +/** + * @param {User[]} users + * @param {boolean} showInviteButton + * @param {Function} inviteFn Called with the user ID. + * @return {Entity[]} + */ +export function fromUsers(users, showInviteButton, inviteFn) { + return users.map(function(u) { + return new UserEntity(u, showInviteButton, inviteFn); + }); +} diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 8915c1412f..64caba0fdf 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -20,7 +20,7 @@ import URL from 'url'; import dis from './dispatcher'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; -import MatrixClientPeg from "./MatrixClientPeg"; +import {MatrixClientPeg} from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 793f5c9227..9131a89e5d 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -16,10 +16,10 @@ limitations under the License. import React from 'react'; import Modal from './Modal'; -import sdk from './'; +import * as sdk from './'; import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import GroupStore from './stores/GroupStore'; import {allSettled} from "./utils/promise"; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 7cdff26a21..236aa0157e 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -29,7 +29,7 @@ import linkifyMatrix from './linkify-matrix'; import _linkifyElement from 'linkifyjs/element'; import _linkifyString from 'linkifyjs/string'; import classNames from 'classnames'; -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import url from 'url'; import EMOJIBASE_REGEX from 'emojibase-regex'; @@ -377,6 +377,7 @@ class TextHighlighter extends BaseHighlighter { * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing * opts.returnString: return an HTML string rather than JSX elements * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer + * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) */ export function bodyToHtml(content, highlights, opts={}) { const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; @@ -459,8 +460,8 @@ export function bodyToHtml(content, highlights, opts={}) { }); return isDisplayedWithHtml ? - : - { strippedBody }; + : + { strippedBody }; } /** diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index c82c93e7a6..72432b9a44 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -16,9 +16,9 @@ limitations under the License. import { createClient, SERVICE_TYPES } from 'matrix-js-sdk'; -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import Modal from './Modal'; -import sdk from './index'; +import * as sdk from './index'; import { _t } from './languageHandler'; import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; import { diff --git a/src/ImageUtils.js b/src/ImageUtils.js index a83d94a633..c0f7b94b81 100644 --- a/src/ImageUtils.js +++ b/src/ImageUtils.js @@ -16,41 +16,38 @@ limitations under the License. 'use strict'; -module.exports = { - - /** - * Returns the actual height that an image of dimensions (fullWidth, fullHeight) - * will occupy if resized to fit inside a thumbnail bounding box of size - * (thumbWidth, thumbHeight). - * - * If the aspect ratio of the source image is taller than the aspect ratio of - * the thumbnail bounding box, then we return the thumbHeight parameter unchanged. - * Otherwise we return the thumbHeight parameter scaled down appropriately to - * reflect the actual height the scaled thumbnail occupies. - * - * This is very useful for calculating how much height a thumbnail will actually - * consume in the timeline, when performing scroll offset calcuations - * (e.g. scroll locking) - */ - thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) { - if (!fullWidth || !fullHeight) { - // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even - // log this because it's spammy - return undefined; - } - if (fullWidth < thumbWidth && fullHeight < thumbHeight) { - // no scaling needs to be applied - return fullHeight; - } - const widthMulti = thumbWidth / fullWidth; - const heightMulti = thumbHeight / fullHeight; - if (widthMulti < heightMulti) { - // width is the dominant dimension so scaling will be fixed on that - return Math.floor(widthMulti * fullHeight); - } else { - // height is the dominant dimension so scaling will be fixed on that - return Math.floor(heightMulti * fullHeight); - } - }, -}; +/** + * Returns the actual height that an image of dimensions (fullWidth, fullHeight) + * will occupy if resized to fit inside a thumbnail bounding box of size + * (thumbWidth, thumbHeight). + * + * If the aspect ratio of the source image is taller than the aspect ratio of + * the thumbnail bounding box, then we return the thumbHeight parameter unchanged. + * Otherwise we return the thumbHeight parameter scaled down appropriately to + * reflect the actual height the scaled thumbnail occupies. + * + * This is very useful for calculating how much height a thumbnail will actually + * consume in the timeline, when performing scroll offset calcuations + * (e.g. scroll locking) + */ +export function thumbHeight(fullWidth, fullHeight, thumbWidth, thumbHeight) { + if (!fullWidth || !fullHeight) { + // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even + // log this because it's spammy + return undefined; + } + if (fullWidth < thumbWidth && fullHeight < thumbHeight) { + // no scaling needs to be applied + return fullHeight; + } + const widthMulti = thumbWidth / fullWidth; + const heightMulti = thumbHeight / fullHeight; + if (widthMulti < heightMulti) { + // width is the dominant dimension so scaling will be fixed on that + return Math.floor(widthMulti * fullHeight); + } else { + // height is the dominant dimension so scaling will be fixed on that + return Math.floor(heightMulti * fullHeight); + } +} diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js index c3de7988b2..65dc7fdb0f 100644 --- a/src/KeyRequestHandler.js +++ b/src/KeyRequestHandler.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import sdk from './index'; +import * as sdk from './index'; import Modal from './Modal'; export default class KeyRequestHandler { @@ -111,6 +111,12 @@ export default class KeyRequestHandler { this._currentUser = null; this._currentDevice = null; + if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) { + // request was removed in the time the dialog was displayed + this._processNextRequest(); + return; + } + if (r) { for (const req of this._pendingKeyRequests[userId][deviceId]) { req.share(); diff --git a/src/Lifecycle.js b/src/Lifecycle.js index b81b563129..0796e326a0 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -18,7 +18,7 @@ limitations under the License. import Matrix from 'matrix-js-sdk'; -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import EventIndexPeg from './indexing/EventIndexPeg'; import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; @@ -28,7 +28,7 @@ import Presence from './Presence'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import Modal from './Modal'; -import sdk from './index'; +import * as sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; import { sendLoginRequest } from "./Login"; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 51ac7acb37..dbc570c872 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd. Copyright 2017, 2018, 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -19,15 +19,15 @@ limitations under the License. import {MatrixClient, MemoryStore} from 'matrix-js-sdk'; -import utils from 'matrix-js-sdk/lib/utils'; -import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; -import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; -import sdk from './index'; +import * as utils from 'matrix-js-sdk/src/utils'; +import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; +import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set'; +import * as sdk from './index'; import createMatrixClient from './utils/createMatrixClient'; import SettingsStore from './settings/SettingsStore'; import MatrixActionCreators from './actions/MatrixActionCreators'; import Modal from './Modal'; -import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; @@ -48,7 +48,7 @@ interface MatrixClientCreds { * This module provides a singleton instance of this class so the 'current' * Matrix Client object is available easily. */ -class MatrixClientPeg { +class _MatrixClientPeg { constructor() { this.matrixClient = null; this._justRegisteredUserId = null; @@ -223,9 +223,10 @@ class MatrixClientPeg { }; opts.cryptoCallbacks = {}; - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); - } + // These are always installed regardless of the labs flag so that + // cross-signing features can toggle on without reloading and also be + // accessed immediately after login. + Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); this.matrixClient = createMatrixClient(opts); @@ -245,6 +246,7 @@ class MatrixClientPeg { } if (!global.mxMatrixClientPeg) { - global.mxMatrixClientPeg = new MatrixClientPeg(); + global.mxMatrixClientPeg = new _MatrixClientPeg(); } -export default global.mxMatrixClientPeg; + +export const MatrixClientPeg = global.mxMatrixClientPeg; diff --git a/src/Modal.js b/src/Modal.js index 4fc9fdcb02..29d3af2e74 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -20,7 +20,7 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import Analytics from './Analytics'; -import sdk from './index'; +import * as sdk from './index'; import dis from './dispatcher'; import { _t } from './languageHandler'; import {defer} from "./utils/promise"; diff --git a/src/Notifier.js b/src/Notifier.js index dd691d8ca7..b030f1b6f9 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -16,13 +16,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import PlatformPeg from './PlatformPeg'; -import TextForEvent from './TextForEvent'; +import * as TextForEvent from './TextForEvent'; import Analytics from './Analytics'; -import Avatar from './Avatar'; +import * as Avatar from './Avatar'; import dis from './dispatcher'; -import sdk from './index'; +import * as sdk from './index'; import { _t } from './languageHandler'; import Modal from './Modal'; import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; @@ -364,4 +364,4 @@ if (!global.mxNotifier) { global.mxNotifier = Notifier; } -module.exports = global.mxNotifier; +export default global.mxNotifier; diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js index 07d8b465af..24dfe61d68 100644 --- a/src/ObjectUtils.js +++ b/src/ObjectUtils.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 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. @@ -22,7 +23,7 @@ limitations under the License. * @return {Object[]} An array of objects with the form: * { key: $KEY, val: $VALUE, place: "add|del" } */ -module.exports.getKeyValueArrayDiffs = function(before, after) { +export function getKeyValueArrayDiffs(before, after) { const results = []; const delta = {}; Object.keys(before).forEach(function(beforeKey) { @@ -76,7 +77,7 @@ module.exports.getKeyValueArrayDiffs = function(before, after) { }); return results; -}; +} /** * Shallow-compare two objects for equality: each key and value must be identical @@ -84,7 +85,7 @@ module.exports.getKeyValueArrayDiffs = function(before, after) { * @param {Object} objB Second object to compare against the first * @return {boolean} whether the two objects have same key=values */ -module.exports.shallowEqual = function(objA, objB) { +export function shallowEqual(objA, objB) { if (objA === objB) { return true; } @@ -109,4 +110,4 @@ module.exports.shallowEqual = function(objA, objB) { } return true; -}; +} diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 31339eb4e5..320599f6d9 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -25,7 +25,7 @@ import { _t } from './languageHandler'; * the client owns the given email address, which is then passed to the password * API on the homeserver in question with the new password. */ -class PasswordReset { +export default class PasswordReset { /** * Configure the endpoints for password resetting. * @param {string} homeserverUrl The URL to the HS which has the account to reset. @@ -101,4 +101,3 @@ class PasswordReset { } } -module.exports = PasswordReset; diff --git a/src/PlatformPeg.js b/src/PlatformPeg.js index 5c1112e23b..34131fde7d 100644 --- a/src/PlatformPeg.js +++ b/src/PlatformPeg.js @@ -47,4 +47,4 @@ class PlatformPeg { if (!global.mxPlatformPeg) { global.mxPlatformPeg = new PlatformPeg(); } -module.exports = global.mxPlatformPeg; +export default global.mxPlatformPeg; diff --git a/src/Presence.js b/src/Presence.js index 8ef988f171..2fc13a090b 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2019 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. @@ -15,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from "./MatrixClientPeg"; +import {MatrixClientPeg} from "./MatrixClientPeg"; import dis from "./dispatcher"; import Timer from './utils/Timer'; @@ -104,4 +105,4 @@ class Presence { } } -module.exports = new Presence(); +export default new Presence(); diff --git a/src/Registration.js b/src/Registration.js index 42e172ca0b..ac8baa3cca 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -21,10 +21,10 @@ limitations under the License. */ import dis from './dispatcher'; -import sdk from './index'; +import * as sdk from './index'; import Modal from './Modal'; import { _t } from './languageHandler'; -// import MatrixClientPeg from './MatrixClientPeg'; +// import {MatrixClientPeg} from './MatrixClientPeg'; // Regex for what a "safe" or "Matrix-looking" localpart would be. // TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514 diff --git a/src/Resend.js b/src/Resend.js index 51ec804c01..6d6c18cf27 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 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. @@ -14,26 +15,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import dis from './dispatcher'; import { EventStatus } from 'matrix-js-sdk'; -module.exports = { - resendUnsentEvents: function(room) { +export default class Resend { + static resendUnsentEvents(room) { room.getPendingEvents().filter(function(ev) { return ev.status === EventStatus.NOT_SENT; }).forEach(function(event) { - module.exports.resend(event); + Resend.resend(event); }); - }, - cancelUnsentEvents: function(room) { + } + + static cancelUnsentEvents(room) { room.getPendingEvents().filter(function(ev) { return ev.status === EventStatus.NOT_SENT; }).forEach(function(event) { - module.exports.removeFromQueue(event); + Resend.removeFromQueue(event); }); - }, - resend: function(event) { + } + + static resend(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ @@ -43,15 +46,16 @@ module.exports = { }, function(err) { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 - console.log('Resend got send failure: ' + err.name + '('+err+')'); + console.log('Resend got send failure: ' + err.name + '(' + err + ')'); dis.dispatch({ action: 'message_send_failed', event: event, }); }); - }, - removeFromQueue: function(event) { + } + + static removeFromQueue(event) { MatrixClientPeg.get().cancelPendingEvent(event); - }, -}; + } +} diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 48baad5d9f..2fe64c994f 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -16,15 +16,16 @@ limitations under the License. */ import React from 'react'; -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import MultiInviter from './utils/MultiInviter'; import Modal from './Modal'; import { getAddressType } from './UserAddress'; import createRoom from './createRoom'; -import sdk from './'; +import * as sdk from './'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; +import SettingsStore from "./settings/SettingsStore"; /** * Invites multiple addresses to a room @@ -41,6 +42,18 @@ function inviteMultipleToRoom(roomId, addrs) { } export function showStartChatInviteDialog() { + if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) { + const DMInviteDialog = sdk.getComponent("dialogs.DMInviteDialog"); + Modal.createTrackedDialog('Start DM', '', DMInviteDialog, { + onFinished: (inviteIds) => { + // TODO: Replace _onStartDmFinished with less hacks + if (inviteIds.length > 0) _onStartDmFinished(true, inviteIds.map(i => ({address: i}))); + // else ignore and just do nothing + }, + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + return; + } + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, { @@ -99,7 +112,7 @@ export function isValid3pidInvite(event) { return true; } -// TODO: Immutable DMs replaces this +// TODO: Canonical DMs replaces this function _onStartDmFinished(shouldInvite, addrs) { if (!shouldInvite) return; diff --git a/src/RoomListSorter.js b/src/RoomListSorter.js index c06cc60c97..0ff37a6af2 100644 --- a/src/RoomListSorter.js +++ b/src/RoomListSorter.js @@ -24,12 +24,8 @@ function tsOfNewestEvent(room) { } } -function mostRecentActivityFirst(roomList) { +export function mostRecentActivityFirst(roomList) { return roomList.sort(function(a, b) { return tsOfNewestEvent(b) - tsOfNewestEvent(a); }); } - -module.exports = { - mostRecentActivityFirst, -}; diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 5bef4afd25..c67acaf314 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -15,8 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from './MatrixClientPeg'; -import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; +import {MatrixClientPeg} from './MatrixClientPeg'; +import {PushProcessor} from 'matrix-js-sdk/src/pushprocessor'; export const ALL_MESSAGES_LOUD = 'all_messages_loud'; export const ALL_MESSAGES = 'all_messages'; diff --git a/src/Rooms.js b/src/Rooms.js index 239e348b58..f65e0ff218 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; /** * Given a room object, return the alias we should use for it, diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 92f0ff6340..819fe3c998 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -18,12 +18,11 @@ limitations under the License. import url from 'url'; import SettingsStore from "./settings/SettingsStore"; import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; -const request = require('browser-request'); - -const SdkConfig = require('./SdkConfig'); -const MatrixClientPeg = require('./MatrixClientPeg'); +import {MatrixClientPeg} from "./MatrixClientPeg"; +import request from "browser-request"; import * as Matrix from 'matrix-js-sdk'; +import SdkConfig from "./SdkConfig"; // The version of the integration manager API we're intending to work with const imApiVersion = "1.1"; diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index c0ffc3022d..2211e513c3 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -232,7 +232,7 @@ Example: } */ -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import { MatrixEvent } from 'matrix-js-sdk'; import dis from './dispatcher'; import WidgetUtils from './utils/WidgetUtils'; @@ -658,30 +658,29 @@ const onMessage = function(event) { let listenerCount = 0; let openManagerUrl = null; -module.exports = { - startListening: function() { - if (listenerCount === 0) { - window.addEventListener("message", onMessage, false); - } - listenerCount += 1; - }, - stopListening: function() { - listenerCount -= 1; - if (listenerCount === 0) { - window.removeEventListener("message", onMessage); - } - if (listenerCount < 0) { - // Make an error so we get a stack trace - const e = new Error( - "ScalarMessaging: mismatched startListening / stopListening detected." + - " Negative count", - ); - console.error(e); - } - }, +export function startListening() { + if (listenerCount === 0) { + window.addEventListener("message", onMessage, false); + } + listenerCount += 1; +} - setOpenManagerUrl: function(url) { - openManagerUrl = url; - }, -}; +export function stopListening() { + listenerCount -= 1; + if (listenerCount === 0) { + window.removeEventListener("message", onMessage); + } + if (listenerCount < 0) { + // Make an error so we get a stack trace + const e = new Error( + "ScalarMessaging: mismatched startListening / stopListening detected." + + " Negative count", + ); + console.error(e); + } +} + +export function setOpenManagerUrl(url) { + openManagerUrl = url; +} diff --git a/src/SdkConfig.js b/src/SdkConfig.ts similarity index 70% rename from src/SdkConfig.js rename to src/SdkConfig.ts index eb18dad453..8177a6c5b8 100644 --- a/src/SdkConfig.js +++ b/src/SdkConfig.ts @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 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. @@ -14,7 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -export const DEFAULTS = { +export interface ConfigOptions { + [key: string]: any; +} + +export const DEFAULTS: ConfigOptions = { // URL to a page we show in an iframe to configure integrations integrations_ui_url: "https://scalar.vector.im/", // Base URL to the REST interface of the integrations server @@ -23,30 +28,37 @@ export const DEFAULTS = { bug_report_endpoint_url: null, }; -class SdkConfig { - static get() { - return global.mxReactSdkConfig || {}; +export default class SdkConfig { + private static instance: ConfigOptions; + + private static setInstance(i: ConfigOptions) { + SdkConfig.instance = i; + + // For debugging purposes + (window).mxReactSdkConfig = i; } - static put(cfg) { + static get() { + return SdkConfig.instance || {}; + } + + static put(cfg: ConfigOptions) { const defaultKeys = Object.keys(DEFAULTS); for (let i = 0; i < defaultKeys.length; ++i) { if (cfg[defaultKeys[i]] === undefined) { cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]]; } } - global.mxReactSdkConfig = cfg; + SdkConfig.setInstance(cfg); } static unset() { - global.mxReactSdkConfig = undefined; + SdkConfig.setInstance({}); } - static add(cfg) { + static add(cfg: ConfigOptions) { const liveConfig = SdkConfig.get(); const newConfig = Object.assign({}, liveConfig, cfg); SdkConfig.put(newConfig); } } - -module.exports = SdkConfig; diff --git a/src/Searching.js b/src/Searching.js index f8976c92e4..a5d945f64b 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -15,7 +15,7 @@ limitations under the License. */ import EventIndexPeg from "./indexing/EventIndexPeg"; -import MatrixClientPeg from "./MatrixClientPeg"; +import {MatrixClientPeg} from "./MatrixClientPeg"; function serverSideSearch(term, roomId = undefined) { let filter; diff --git a/src/Skinner.js b/src/Skinner.js index 1fe12f85ab..3baecc9fb3 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -28,21 +28,37 @@ class Skinner { " b) A component has called getComponent at the root level", ); } - let comp = this.components[name]; - // XXX: Temporarily also try 'views.' as we're currently - // leaving the 'views.' off views. + + const doLookup = (components) => { + if (!components) return null; + let comp = components[name]; + // XXX: Temporarily also try 'views.' as we're currently + // leaving the 'views.' off views. + if (!comp) { + comp = components['views.' + name]; + } + return comp; + }; + + // Check the skin first + let comp = doLookup(this.components); + + // If that failed, check against our own components if (!comp) { - comp = this.components['views.'+name]; + // Lazily load our own components because they might end up calling .getComponent() + comp = doLookup(require("./component-index").components); } + // Just return nothing instead of erroring - the consumer should be smart enough to + // handle this at this point. if (!comp) { - throw new Error("No such component: "+name); + return null; } // components have to be functions. const validType = typeof comp === 'function'; if (!validType) { - throw new Error(`Not a valid component: ${name}.`); + throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`); } return comp; } @@ -90,5 +106,5 @@ class Skinner { if (global.mxSkinner === undefined) { global.mxSkinner = new Skinner(); } -module.exports = global.mxSkinner; +export default global.mxSkinner; diff --git a/src/SlashCommands.js b/src/SlashCommands.js index a9c015fdaf..20b8ba76da 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -18,9 +18,9 @@ limitations under the License. import React from 'react'; -import MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import dis from './dispatcher'; -import sdk from './index'; +import * as sdk from './index'; import {_t, _td} from './languageHandler'; import Modal from './Modal'; import MultiInviter from './utils/MultiInviter'; @@ -780,54 +780,52 @@ export const CommandMap = { const deviceId = matches[2]; const fingerprint = matches[3]; - return success( - // Promise.resolve to handle transition from static result to promise; can be removed - // in future - Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => { - if (!device) { - throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); - } + return success((async () => { + const device = await cli.getStoredDevice(userId, deviceId); + if (!device) { + throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); + } + const deviceTrust = await cli.checkDeviceTrust(userId, deviceId); - if (device.isVerified()) { - if (device.getFingerprint() === fingerprint) { - throw new Error(_t('Device already verified!')); - } else { - throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); - } + if (deviceTrust.isVerified()) { + if (device.getFingerprint() === fingerprint) { + throw new Error(_t('Device already verified!')); + } else { + throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); } + } - if (device.getFingerprint() !== fingerprint) { - const fprint = device.getFingerprint(); - throw new Error( - _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + - ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + - '"%(fingerprint)s". This could mean your communications are being intercepted!', - { - fprint, - userId, - deviceId, - fingerprint, - })); - } + if (device.getFingerprint() !== fingerprint) { + const fprint = device.getFingerprint(); + throw new Error( + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + + '"%(fingerprint)s". This could mean your communications are being intercepted!', + { + fprint, + userId, + deviceId, + fingerprint, + })); + } - return cli.setDeviceVerified(userId, deviceId, true); - }).then(() => { - // Tell the user we verified everything - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); - Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { - title: _t('Verified key'), - description:
-

- { - _t('The signing key you provided matches the signing key you received ' + - 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', - {userId, deviceId}) - } -

-
, - }); - }), - ); + await cli.setDeviceVerified(userId, deviceId, true); + + // Tell the user we verified everything + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { + title: _t('Verified key'), + description:
+

+ { + _t('The signing key you provided matches the signing key you received ' + + 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', + {userId, deviceId}) + } +

+
, + }); + })()); } } return reject(this.getUsage()); diff --git a/src/SlateComposerHistoryManager.js b/src/SlateComposerHistoryManager.js deleted file mode 100644 index 948dcf64ff..0000000000 --- a/src/SlateComposerHistoryManager.js +++ /dev/null @@ -1,86 +0,0 @@ -//@flow -/* -Copyright 2017 Aviral Dasgupta - -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 {Value} from 'slate'; - -import _clamp from 'lodash/clamp'; - -type MessageFormat = 'rich' | 'markdown'; - -class HistoryItem { - // We store history items in their native format to ensure history is accurate - // and then convert them if our RTE has subsequently changed format. - value: Value; - format: MessageFormat = 'rich'; - - constructor(value: ?Value, format: ?MessageFormat) { - this.value = value; - this.format = format; - } - - static fromJSON(obj: Object): HistoryItem { - return new HistoryItem( - Value.fromJSON(obj.value), - obj.format, - ); - } - - toJSON(): Object { - return { - value: this.value.toJSON(), - format: this.format, - }; - } -} - -export default class SlateComposerHistoryManager { - history: Array = []; - prefix: string; - lastIndex: number = 0; // used for indexing the storage - currentIndex: number = 0; // used for indexing the loaded validated history Array - - constructor(roomId: string, prefix: string = 'mx_composer_history_') { - this.prefix = prefix + roomId; - - // TODO: Performance issues? - let item; - for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { - try { - this.history.push( - HistoryItem.fromJSON(JSON.parse(item)), - ); - } catch (e) { - console.warn("Throwing away unserialisable history", e); - } - } - this.lastIndex = this.currentIndex; - // reset currentIndex to account for any unserialisable history - this.currentIndex = this.history.length; - } - - save(value: Value, format: MessageFormat) { - const item = new HistoryItem(value, format); - this.history.push(item); - this.currentIndex = this.history.length; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); - } - - getItem(offset: number): ?HistoryItem { - this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); - return this.history[this.currentIndex]; - } -} diff --git a/src/Terms.js b/src/Terms.js index 14a7ccb65e..6ae89f9a2c 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -16,8 +16,8 @@ limitations under the License. import classNames from 'classnames'; -import MatrixClientPeg from './MatrixClientPeg'; -import sdk from './'; +import {MatrixClientPeg} from './MatrixClientPeg'; +import * as sdk from './'; import Modal from './Modal'; export class TermsNotSignedError extends Error {} diff --git a/src/TextForEvent.js b/src/TextForEvent.js index c3c8396e26..a0d088affb 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -13,7 +13,7 @@ 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 MatrixClientPeg from './MatrixClientPeg'; +import {MatrixClientPeg} from './MatrixClientPeg'; import CallHandler from './CallHandler'; import { _t } from './languageHandler'; import * as Roles from './Roles'; @@ -473,7 +473,7 @@ function textForPowerEvent(event) { } function textForPinnedEvent(event) { - const senderName = event.getSender(); + const senderName = event.sender ? event.sender.name : event.getSender(); return _t("%(senderName)s changed the pinned messages for the room.", {senderName}); } @@ -620,10 +620,8 @@ for (const evType of ALL_RULE_TYPES) { stateHandlers[evType] = textForMjolnirEvent; } -module.exports = { - textForEvent: function(ev) { - const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; - if (handler) return handler(ev); - return ''; - }, -}; +export function textForEvent(ev) { + const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; + if (handler) return handler(ev); + return ''; +} diff --git a/src/Tinter.js b/src/Tinter.js index de9ae94097..24a4d25a00 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -143,10 +143,14 @@ class Tinter { * over time then the best bet is to register a single callback for the * entire set. * + * To ensure the tintable work happens at least once, it is also called as + * part of registration. + * * @param {Function} tintable Function to call when the tint changes. */ registerTintable(tintable) { this.tintables.push(tintable); + tintable(); } getKeyRgb() { diff --git a/src/Unread.js b/src/Unread.js index d5c5993974..ca713b05e4 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -14,80 +14,78 @@ See the License for the specific language governing permissions and limitations under the License. */ -const MatrixClientPeg = require('./MatrixClientPeg'); +import {MatrixClientPeg} from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; -const sdk = require('./index'); +import * as sdk from "./index"; +import {haveTileForEvent} from "./components/views/rooms/EventTile"; -module.exports = { - /** - * Returns true iff this event arriving in a room should affect the room's - * count of unread messages - */ - eventTriggersUnreadCount: function(ev) { - if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { - return false; - } else if (ev.getType() == 'm.room.member') { - return false; - } else if (ev.getType() == 'm.room.third_party_invite') { - return false; - } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { - return false; - } else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { - return false; - } else if (ev.getType() == 'm.room.aliases' || ev.getType() == 'm.room.canonical_alias') { - return false; - } else if (ev.getType() == 'm.room.server_acl') { +/** + * Returns true iff this event arriving in a room should affect the room's + * count of unread messages + */ +export function eventTriggersUnreadCount(ev) { + if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { + return false; + } else if (ev.getType() == 'm.room.member') { + return false; + } else if (ev.getType() == 'm.room.third_party_invite') { + return false; + } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { + return false; + } else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { + return false; + } else if (ev.getType() == 'm.room.aliases' || ev.getType() == 'm.room.canonical_alias') { + return false; + } else if (ev.getType() == 'm.room.server_acl') { + return false; + } + return haveTileForEvent(ev); +} + +export function doesRoomHaveUnreadMessages(room) { + const myUserId = MatrixClientPeg.get().credentials.userId; + + // get the most recent read receipt sent by our account. + // N.B. this is NOT a read marker (RM, aka "read up to marker"), + // despite the name of the method :(( + const readUpToId = room.getEventReadUpTo(myUserId); + + // as we don't send RRs for our own messages, make sure we special case that + // if *we* sent the last message into the room, we consider it not unread! + // Should fix: https://github.com/vector-im/riot-web/issues/3263 + // https://github.com/vector-im/riot-web/issues/2427 + // ...and possibly some of the others at + // https://github.com/vector-im/riot-web/issues/3363 + if (room.timeline.length && + room.timeline[room.timeline.length - 1].sender && + room.timeline[room.timeline.length - 1].sender.userId === myUserId) { + return false; + } + + // this just looks at whatever history we have, which if we've only just started + // up probably won't be very much, so if the last couple of events are ones that + // don't count, we don't know if there are any events that do count between where + // we have and the read receipt. We could fetch more history to try & find out, + // but currently we just guess. + + // Loop through messages, starting with the most recent... + for (let i = room.timeline.length - 1; i >= 0; --i) { + const ev = room.timeline[i]; + + if (ev.getId() == readUpToId) { + // If we've read up to this event, there's nothing more recent + // that counts and we can stop looking because the user's read + // this and everything before. return false; + } else if (!shouldHideEvent(ev) && eventTriggersUnreadCount(ev)) { + // We've found a message that counts before we hit + // the user's read receipt, so this room is definitely unread. + return true; } - const EventTile = sdk.getComponent('rooms.EventTile'); - return EventTile.haveTileForEvent(ev); - }, - - doesRoomHaveUnreadMessages: function(room) { - const myUserId = MatrixClientPeg.get().credentials.userId; - - // get the most recent read receipt sent by our account. - // N.B. this is NOT a read marker (RM, aka "read up to marker"), - // despite the name of the method :(( - const readUpToId = room.getEventReadUpTo(myUserId); - - // as we don't send RRs for our own messages, make sure we special case that - // if *we* sent the last message into the room, we consider it not unread! - // Should fix: https://github.com/vector-im/riot-web/issues/3263 - // https://github.com/vector-im/riot-web/issues/2427 - // ...and possibly some of the others at - // https://github.com/vector-im/riot-web/issues/3363 - if (room.timeline.length && - room.timeline[room.timeline.length - 1].sender && - room.timeline[room.timeline.length - 1].sender.userId === myUserId) { - return false; - } - - // this just looks at whatever history we have, which if we've only just started - // up probably won't be very much, so if the last couple of events are ones that - // don't count, we don't know if there are any events that do count between where - // we have and the read receipt. We could fetch more history to try & find out, - // but currently we just guess. - - // Loop through messages, starting with the most recent... - for (let i = room.timeline.length - 1; i >= 0; --i) { - const ev = room.timeline[i]; - - if (ev.getId() == readUpToId) { - // If we've read up to this event, there's nothing more recent - // that counts and we can stop looking because the user's read - // this and everything before. - return false; - } else if (!shouldHideEvent(ev) && this.eventTriggersUnreadCount(ev)) { - // We've found a message that counts before we hit - // the user's read receipt, so this room is definitely unread. - return true; - } - } - // If we got here, we didn't find a message that counted but didn't find - // the user's read receipt either, so we guess and say that the room is - // unread on the theory that false positives are better than false - // negatives here. - return true; - }, -}; + } + // If we got here, we didn't find a message that counted but didn't find + // the user's read receipt either, so we guess and say that the room is + // unread on the theory that false positives are better than false + // negatives here. + return true; +} diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js index e0e333a371..180dad876b 100644 --- a/src/VectorConferenceHandler.js +++ b/src/VectorConferenceHandler.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 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. @@ -14,9 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {createNewMatrixCall, Room} from "matrix-js-sdk"; +import {createNewMatrixCall as jsCreateNewMatrixCall, Room} from "matrix-js-sdk"; import CallHandler from './CallHandler'; -import MatrixClientPeg from "./MatrixClientPeg"; +import {MatrixClientPeg} from "./MatrixClientPeg"; // FIXME: this is Riot (Vector) specific code, but will be removed shortly when // we switch over to jitsi entirely for video conferencing. @@ -28,10 +29,10 @@ import MatrixClientPeg from "./MatrixClientPeg"; const USER_PREFIX = "fs_"; const DOMAIN = "matrix.org"; -function ConferenceCall(matrixClient, groupChatRoomId) { +export function ConferenceCall(matrixClient, groupChatRoomId) { this.client = matrixClient; this.groupRoomId = groupChatRoomId; - this.confUserId = module.exports.getConferenceUserIdForRoom(this.groupRoomId); + this.confUserId = getConferenceUserIdForRoom(this.groupRoomId); } ConferenceCall.prototype.setup = function() { @@ -42,7 +43,7 @@ ConferenceCall.prototype.setup = function() { // return a call for *this* room to be placed. We also tack on // confUserId to speed up lookups (else we'd need to loop every room // looking for a 1:1 room with this conf user ID!) - const call = createNewMatrixCall(self.client, room.roomId); + const call = jsCreateNewMatrixCall(self.client, room.roomId); call.confUserId = self.confUserId; call.groupRoomId = self.groupRoomId; return call; @@ -90,7 +91,7 @@ ConferenceCall.prototype._getConferenceUserRoom = function() { * @param {string} userId The user ID to check. * @return {boolean} True if it is a conference bot. */ -module.exports.isConferenceUser = function(userId) { +export function isConferenceUser(userId) { if (userId.indexOf("@" + USER_PREFIX) !== 0) { return false; } @@ -101,26 +102,26 @@ module.exports.isConferenceUser = function(userId) { return /^!.+:.+/.test(decoded); } return false; -}; +} -module.exports.getConferenceUserIdForRoom = function(roomId) { +export function getConferenceUserIdForRoom(roomId) { // abuse browserify's core node Buffer support (strip padding ='s) const base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, ""); return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN; -}; +} -module.exports.createNewMatrixCall = function(client, roomId) { +export function createNewMatrixCall(client, roomId) { const confCall = new ConferenceCall( client, roomId, ); return confCall.setup(); -}; +} -module.exports.getConferenceCallForRoom = function(roomId) { +export function getConferenceCallForRoom(roomId) { // search for a conference 1:1 call for this group chat room ID const activeCall = CallHandler.getAnyActiveCall(); if (activeCall && activeCall.confUserId) { - const thisRoomConfUserId = module.exports.getConferenceUserIdForRoom( + const thisRoomConfUserId = getConferenceUserIdForRoom( roomId, ); if (thisRoomConfUserId === activeCall.confUserId) { @@ -128,8 +129,7 @@ module.exports.getConferenceCallForRoom = function(roomId) { } } return null; -}; +} -module.exports.ConferenceCall = ConferenceCall; - -module.exports.slot = 'conference'; +// TODO: Document this. +export const slot = 'conference'; diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 245ca6648b..ce52f60dbd 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -1,7 +1,7 @@ -const React = require('react'); -const ReactDom = require('react-dom'); +import React from "react"; +import ReactDom from "react-dom"; +import Velocity from "velocity-animate"; import PropTypes from 'prop-types'; -const Velocity = require('velocity-animate'); /** * The Velociraptor contains components and animates transitions with velocity. diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js index db216f81fb..ffbf7de829 100644 --- a/src/VelocityBounce.js +++ b/src/VelocityBounce.js @@ -1,4 +1,4 @@ -const Velocity = require('velocity-animate'); +import Velocity from "velocity-animate"; // courtesy of https://github.com/julianshapiro/velocity/issues/283 // We only use easeOutBounce (easeInBounce is just sort of nonsensical) diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index eb09685cbe..d11cddf487 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -14,71 +14,69 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from "./MatrixClientPeg"; +import {MatrixClientPeg} from "./MatrixClientPeg"; import { _t } from './languageHandler'; -module.exports = { - usersTypingApartFromMeAndIgnored: function(room) { - return this.usersTyping( - room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()), - ); - }, +export function usersTypingApartFromMeAndIgnored(room) { + return usersTyping( + room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()), + ); +} - usersTypingApartFromMe: function(room) { - return this.usersTyping( - room, [MatrixClientPeg.get().credentials.userId], - ); - }, +export function usersTypingApartFromMe(room) { + return usersTyping( + room, [MatrixClientPeg.get().credentials.userId], + ); +} - /** - * Given a Room object and, optionally, a list of userID strings - * to exclude, return a list of user objects who are typing. - * @param {Room} room: room object to get users from. - * @param {string[]} exclude: list of user mxids to exclude. - * @returns {string[]} list of user objects who are typing. - */ - usersTyping: function(room, exclude) { - const whoIsTyping = []; +/** + * Given a Room object and, optionally, a list of userID strings + * to exclude, return a list of user objects who are typing. + * @param {Room} room: room object to get users from. + * @param {string[]} exclude: list of user mxids to exclude. + * @returns {string[]} list of user objects who are typing. + */ +export function usersTyping(room, exclude) { + const whoIsTyping = []; - if (exclude === undefined) { - exclude = []; - } + if (exclude === undefined) { + exclude = []; + } - const memberKeys = Object.keys(room.currentState.members); - for (let i = 0; i < memberKeys.length; ++i) { - const userId = memberKeys[i]; + const memberKeys = Object.keys(room.currentState.members); + for (let i = 0; i < memberKeys.length; ++i) { + const userId = memberKeys[i]; - if (room.currentState.members[userId].typing) { - if (exclude.indexOf(userId) === -1) { - whoIsTyping.push(room.currentState.members[userId]); - } + if (room.currentState.members[userId].typing) { + if (exclude.indexOf(userId) === -1) { + whoIsTyping.push(room.currentState.members[userId]); } } + } - return whoIsTyping; - }, + return whoIsTyping; +} - whoIsTypingString: function(whoIsTyping, limit) { - let othersCount = 0; - if (whoIsTyping.length > limit) { - othersCount = whoIsTyping.length - limit + 1; - } - if (whoIsTyping.length === 0) { - return ''; - } else if (whoIsTyping.length === 1) { - return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name}); - } - const names = whoIsTyping.map(function(m) { - return m.name; +export function whoIsTypingString(whoIsTyping, limit) { + let othersCount = 0; + if (whoIsTyping.length > limit) { + othersCount = whoIsTyping.length - limit + 1; + } + if (whoIsTyping.length === 0) { + return ''; + } else if (whoIsTyping.length === 1) { + return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name}); + } + const names = whoIsTyping.map(function(m) { + return m.name; + }); + if (othersCount>=1) { + return _t('%(names)s and %(count)s others are typing …', { + names: names.slice(0, limit - 1).join(', '), + count: othersCount, }); - if (othersCount>=1) { - return _t('%(names)s and %(count)s others are typing …', { - names: names.slice(0, limit - 1).join(', '), - count: othersCount, - }); - } else { - const lastPerson = names.pop(); - return _t('%(names)s and %(lastPerson)s are typing …', {names: names.join(', '), lastPerson: lastPerson}); - } - }, -}; + } else { + const lastPerson = names.pop(); + return _t('%(names)s and %(lastPerson)s are typing …', {names: names.join(', '), lastPerson: lastPerson}); + } +} diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 1d8e1b9cd3..d40a8ab637 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -23,7 +23,7 @@ limitations under the License. import FromWidgetPostMessageApi from './FromWidgetPostMessageApi'; import ToWidgetPostMessageApi from './ToWidgetPostMessageApi'; import Modal from "./Modal"; -import MatrixClientPeg from "./MatrixClientPeg"; +import {MatrixClientPeg} from "./MatrixClientPeg"; import SettingsStore from "./settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; import WidgetUtils from "./utils/WidgetUtils"; diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index e5911c4e32..d534fe5d1d 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -16,11 +16,10 @@ limitations under the License. import { asyncAction } from './actionCreators'; import RoomListStore from '../stores/RoomListStore'; - import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; -import sdk from '../index'; +import * as sdk from '../index'; const RoomListActions = {}; diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index 15bb1e046b..f6e17b1c84 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -14,16 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Key} from "../../../Keyboard"; - -const React = require("react"); +import React from "react"; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -const sdk = require('../../../index'); -const MatrixClientPeg = require("../../../MatrixClientPeg"); +import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import {Key} from "../../../Keyboard"; +import * as sdk from "../../../index"; -module.exports = createReactClass({ +// XXX: This component is not cross-signing aware. +// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this +// component or taking it out to pasture. +export default createReactClass({ displayName: 'EncryptedEventDialog', propTypes: { diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index ba2e985889..481075d0fa 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler'; import { MatrixClient } from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; const PHASE_EDIT = 1; const PHASE_EXPORTING = 2; diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index de9e819f5a..591c84f5d3 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -20,7 +20,7 @@ import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; function readFileAsArrayBuffer(file) { diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 3fac00c1b3..8940239cfd 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -1,6 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -17,11 +17,13 @@ limitations under the License. import React from 'react'; import FileSaver from 'file-saver'; - -import sdk from '../../../../index'; -import MatrixClientPeg from '../../../../MatrixClientPeg'; +import * as sdk from '../../../../index'; +import {MatrixClientPeg} from '../../../../MatrixClientPeg'; +import PropTypes from 'prop-types'; import { scorePassword } from '../../../../utils/PasswordScorer'; import { _t } from '../../../../languageHandler'; +import { accessSecretStorage } from '../../../../CrossSigningManager'; +import SettingsStore from '../../../../settings/SettingsStore'; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; @@ -49,10 +51,20 @@ function selectText(target) { * on the server. */ export default class CreateKeyBackupDialog extends React.PureComponent { + static propTypes = { + secureSecretStorage: PropTypes.bool, + onFinished: PropTypes.func.isRequired, + } + constructor(props) { super(props); + this._recoveryKeyNode = null; + this._keyBackupInfo = null; + this._setZxcvbnResultTimeout = null; + this.state = { + secureSecretStorage: props.secureSecretStorage, phase: PHASE_PASSPHRASE, passPhrase: '', passPhraseConfirm: '', @@ -61,12 +73,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent { zxcvbnResult: null, setPassPhrase: false, }; + + if (this.state.secureSecretStorage === undefined) { + this.state.secureSecretStorage = + SettingsStore.isFeatureEnabled("feature_cross_signing"); + } + + // If we're using secret storage, skip ahead to the backing up step, as + // `accessSecretStorage` will handle passphrases as needed. + if (this.state.secureSecretStorage) { + this.state.phase = PHASE_BACKINGUP; + } } - componentWillMount() { - this._recoveryKeyNode = null; - this._keyBackupInfo = null; - this._setZxcvbnResultTimeout = null; + componentDidMount() { + // If we're using secret storage, skip ahead to the backing up step, as + // `accessSecretStorage` will handle passphrases as needed. + if (this.state.secureSecretStorage) { + this._createBackup(); + } } componentWillUnmount() { @@ -103,15 +128,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent { } _createBackup = async () => { + const { secureSecretStorage } = this.state; this.setState({ phase: PHASE_BACKINGUP, error: null, }); let info; try { - info = await MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ); + if (secureSecretStorage) { + await accessSecretStorage(async () => { + info = await MatrixClientPeg.get().prepareKeyBackupVersion( + null /* random key */, + { secureSecretStorage: true }, + ); + info = await MatrixClientPeg.get().createKeyBackupVersion(info); + }); + } else { + info = await MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ); + } await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); this.setState({ phase: PHASE_DONE, diff --git a/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js b/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js index a9df3cca6e..b79911c66e 100644 --- a/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js +++ b/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js @@ -16,7 +16,7 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; -import sdk from "../../../../index"; +import * as sdk from "../../../../index"; import { _t } from "../../../../languageHandler"; export default class IgnoreRecoveryReminderDialog extends React.PureComponent { diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 28281af771..559d972f24 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -1,5 +1,6 @@ /* -Copyright 2018-2019 New Vector Ltd +Copyright 2018, 2019 New Vector Ltd +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. @@ -16,8 +17,8 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; -import sdk from "../../../../index"; -import MatrixClientPeg from '../../../../MatrixClientPeg'; +import * as sdk from "../../../../index"; +import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import dis from "../../../../dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; @@ -40,9 +41,11 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onSetupClick = async () => { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { - onFinished: this.props.onFinished, - }); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, { + onFinished: this.props.onFinished, + }, null, /* priority = */ false, /* static = */ true, + ); } render() { diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js index 1975fbe6d6..9dfc3e73ed 100644 --- a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +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. @@ -16,7 +17,7 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; -import sdk from "../../../../index"; +import * as sdk from "../../../../index"; import dis from "../../../../dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; @@ -35,6 +36,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { this.props.onFinished(); Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("./CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 25bc8cdfda..01b9c9c7c8 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -16,22 +16,23 @@ limitations under the License. */ import React from 'react'; -import sdk from '../../../../index'; -import MatrixClientPeg from '../../../../MatrixClientPeg'; +import * as sdk from '../../../../index'; +import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; const PHASE_LOADING = 0; -const PHASE_MIGRATE = 1; -const PHASE_PASSPHRASE = 2; -const PHASE_PASSPHRASE_CONFIRM = 3; -const PHASE_SHOWKEY = 4; -const PHASE_KEEPITSAFE = 5; -const PHASE_STORING = 6; -const PHASE_DONE = 7; -const PHASE_OPTOUT_CONFIRM = 8; +const PHASE_RESTORE_KEY_BACKUP = 1; +const PHASE_MIGRATE = 2; +const PHASE_PASSPHRASE = 3; +const PHASE_PASSPHRASE_CONFIRM = 4; +const PHASE_SHOWKEY = 5; +const PHASE_KEEPITSAFE = 6; +const PHASE_STORING = 7; +const PHASE_DONE = 8; +const PHASE_OPTOUT_CONFIRM = 9; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. @@ -67,6 +68,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { downloaded: false, zxcvbnResult: null, setPassPhrase: false, + backupInfo: null, + backupSigStatus: null, }; this._fetchBackupInfo(); @@ -80,10 +83,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { async _fetchBackupInfo() { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + + const phase = backupInfo ? + (backupSigStatus.usable ? PHASE_MIGRATE : PHASE_RESTORE_KEY_BACKUP) : + PHASE_PASSPHRASE; this.setState({ - phase: backupInfo ? PHASE_MIGRATE: PHASE_PASSPHRASE, + phase, backupInfo, + backupSigStatus, }); } @@ -161,6 +170,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.props.onFinished(true); } + _onRestoreKeyBackupClick = () => { + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); + } + _onOptOutClick = () => { this.setState({phase: PHASE_OPTOUT_CONFIRM}); } @@ -268,6 +285,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; } + _renderPhaseRestoreKeyBackup() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Key Backup is enabled on your account but has not been set " + + "up from this session. To set up secret storage, " + + "restore your key backup.", + )}

+ + +
; + } + _renderPhaseMigrate() { // TODO: This is a temporary screen so people who have the labs flag turned on and // click the button are aware they're making a change to their account. @@ -277,9 +311,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( - "Secret Storage will be set up using your existing key backup details." + + "Secret Storage will be set up using your existing key backup details. " + "Your secret storage passphrase and recovery key will be the same as " + - " they were for your key backup", + "they were for your key backup.", )}

{ + async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); // Disable autocompletions when composing commands because of various issues diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 49ef7dfb43..ca1b1478cc 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -37,7 +37,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; } - async getCompletions(query: string, selection: SelectionRange, force?: boolean = false) { + async getCompletions(query: string, selection: SelectionRange, force: boolean = false) { const {command, range} = this.getCurrentCommand(query, selection); if (!query || !command) { return []; diff --git a/src/autocomplete/NotifProvider.js b/src/autocomplete/NotifProvider.js index 95cfb34616..e7c8f6f70d 100644 --- a/src/autocomplete/NotifProvider.js +++ b/src/autocomplete/NotifProvider.js @@ -17,9 +17,9 @@ limitations under the License. import React from 'react'; import AutocompleteProvider from './AutocompleteProvider'; import { _t } from '../languageHandler'; -import MatrixClientPeg from '../MatrixClientPeg'; +import {MatrixClientPeg} from '../MatrixClientPeg'; import {PillCompletion} from './Components'; -import sdk from '../index'; +import * as sdk from '../index'; import type {Completion, SelectionRange} from "./Autocompleter"; const AT_ROOM_REGEX = /@\S*/g; @@ -30,7 +30,7 @@ export default class NotifProvider extends AutocompleteProvider { this.room = room; } - async getCompletions(query: string, selection: SelectionRange, force?:boolean = false): Array { + async getCompletions(query: string, selection: SelectionRange, force:boolean = false): Array { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const client = MatrixClientPeg.get(); diff --git a/src/autocomplete/PlainWithPillsSerializer.js b/src/autocomplete/PlainWithPillsSerializer.js deleted file mode 100644 index 09bb3772ac..0000000000 --- a/src/autocomplete/PlainWithPillsSerializer.js +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -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. -*/ - -// Based originally on slate-plain-serializer - -import { Block } from 'slate'; - -/** - * Plain text serializer, which converts a Slate `value` to a plain text string, - * serializing pills into various different formats as required. - * - * @type {PlainWithPillsSerializer} - */ - -class PlainWithPillsSerializer { - /* - * @param {String} options.pillFormat - either 'md', 'plain', 'id' - */ - constructor(options = {}) { - const { - pillFormat = 'plain', - } = options; - this.pillFormat = pillFormat; - } - - /** - * Serialize a Slate `value` to a plain text string, - * serializing pills as either MD links, plain text representations or - * ID representations as required. - * - * @param {Value} value - * @return {String} - */ - serialize = value => { - return this._serializeNode(value.document); - } - - /** - * Serialize a `node` to plain text. - * - * @param {Node} node - * @return {String} - */ - _serializeNode = node => { - if ( - node.object == 'document' || - (node.object == 'block' && Block.isBlockList(node.nodes)) - ) { - return node.nodes.map(this._serializeNode).join('\n'); - } else if (node.type == 'emoji') { - return node.data.get('emojiUnicode'); - } else if (node.type == 'pill') { - const completion = node.data.get('completion'); - // over the wire the @room pill is just plaintext - if (completion === '@room') return completion; - - switch (this.pillFormat) { - case 'plain': - return completion; - case 'md': - return `[${ completion }](${ node.data.get('href') })`; - case 'id': - return node.data.get('completionId') || completion; - } - } else if (node.nodes) { - return node.nodes.map(this._serializeNode).join(''); - } else { - return node.text; - } - } -} - -/** - * Export. - * - * @type {PlainWithPillsSerializer} - */ - -export default PlainWithPillsSerializer; diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index b67abc388e..b28c79ac54 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -20,11 +20,11 @@ limitations under the License. import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import MatrixClientPeg from '../MatrixClientPeg'; +import {MatrixClientPeg} from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../Rooms'; -import sdk from '../index'; +import * as sdk from '../index'; import _sortBy from 'lodash/sortBy'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import type {Completion, SelectionRange} from "./Autocompleter"; @@ -48,7 +48,7 @@ export default class RoomProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array { + async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); const client = MatrixClientPeg.get(); diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index ac159c8213..7fd600b136 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -22,10 +22,10 @@ import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {PillCompletion} from './Components'; -import sdk from '../index'; +import * as sdk from '../index'; import QueryMatcher from './QueryMatcher'; import _sortBy from 'lodash/sortBy'; -import MatrixClientPeg from '../MatrixClientPeg'; +import {MatrixClientPeg} from '../MatrixClientPeg'; import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk'; import {makeUserPermalink} from "../utils/permalinks/Permalinks"; @@ -91,7 +91,7 @@ export default class UserProvider extends AutocompleteProvider { this.users = null; } - async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array { + async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); // lazy-load user list into matcher diff --git a/src/components/structures/CompatibilityPage.js b/src/components/structures/CompatibilityPage.js index 28c86f8dd8..9a3fdb5f39 100644 --- a/src/components/structures/CompatibilityPage.js +++ b/src/components/structures/CompatibilityPage.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 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. @@ -20,7 +21,7 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'CompatibilityPage', propTypes: { onAccept: PropTypes.func, diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index 662972ee37..b4b1b80163 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -21,7 +21,7 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {Key} from "../../Keyboard"; -import sdk from "../../index"; +import * as sdk from "../../index"; import AccessibleButton from "../views/elements/AccessibleButton"; // Shamelessly ripped off Modal.js. There's probably a better way diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index f1b548d72f..e8ff6e814e 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import CustomRoomTagStore from '../../stores/CustomRoomTagStore'; import AutoHideScrollbar from './AutoHideScrollbar'; -import sdk from '../../index'; +import * as sdk from '../../index'; import dis from '../../dispatcher'; import classNames from 'classnames'; import * as FormattingUtils from '../../utils/FormattingUtils'; diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index 63767255e2..6d734c3838 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -23,9 +23,9 @@ import PropTypes from 'prop-types'; import request from 'browser-request'; import { _t } from '../../languageHandler'; import sanitizeHtml from 'sanitize-html'; -import sdk from '../../index'; +import * as sdk from '../../index'; import dis from '../../dispatcher'; -import MatrixClientPeg from '../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../MatrixClientPeg'; import classnames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index f5a5912dd5..61b3d2d4b9 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2019 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. @@ -19,8 +20,8 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import Matrix from 'matrix-js-sdk'; -import sdk from '../../index'; -import MatrixClientPeg from '../../MatrixClientPeg'; +import * as sdk from '../../index'; +import {MatrixClientPeg} from '../../MatrixClientPeg'; import { _t } from '../../languageHandler'; /* @@ -126,4 +127,4 @@ const FilePanel = createReactClass({ }, }); -module.exports = FilePanel; +export default FilePanel; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 9df4630136..5ae0699a2f 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -19,8 +19,8 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import MatrixClientPeg from '../../MatrixClientPeg'; -import sdk from '../../index'; +import {MatrixClientPeg} from '../../MatrixClientPeg'; +import * as sdk from '../../index'; import dis from '../../dispatcher'; import { getHostingLink } from '../../utils/HostingLink'; import { sanitizedHtmlNode } from '../../HtmlUtils'; @@ -1299,7 +1299,7 @@ export default createReactClass({ ); } - const rightPanel = !RightPanelStore.getSharedInstance().isOpenForGroup + const rightPanel = RightPanelStore.getSharedInstance().isOpenForGroup ? : undefined; diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 1981310a2f..53bb990e26 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -15,16 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import Matrix from 'matrix-js-sdk'; -const InteractiveAuth = Matrix.InteractiveAuth; - +import {InteractiveAuth} from "matrix-js-sdk"; import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {getEntryComponentForLoginType} from '../views/auth/InteractiveAuthEntryComponents'; -import sdk from '../../index'; +import * as sdk from '../../index'; export default createReactClass({ displayName: 'InteractiveAuth', diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 796840a625..8a7d10e5b5 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -20,9 +20,9 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Key } from '../../Keyboard'; -import sdk from '../../index'; +import * as sdk from '../../index'; import dis from '../../dispatcher'; -import VectorConferenceHandler from '../../VectorConferenceHandler'; +import * as VectorConferenceHandler from '../../VectorConferenceHandler'; import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import Analytics from "../../Analytics"; @@ -301,4 +301,4 @@ const LeftPanel = createReactClass({ }, }); -module.exports = LeftPanel; +export default LeftPanel; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 7261af3bf0..9597f99cd2 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -26,10 +26,10 @@ import { Key, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import PageTypes from '../../PageTypes'; import CallMediaHandler from '../../CallMediaHandler'; import { fixupColorFonts } from '../../utils/FontManager'; -import sdk from '../../index'; +import * as sdk from '../../index'; import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; -import MatrixClientPeg from '../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "../../stores/RoomListStore"; import { getHomePageUrl } from '../../utils/pages'; @@ -393,13 +393,6 @@ const LoggedInView = createReactClass({ return; } - // XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036 - // If using Slate, consume the Backspace without first focusing as it causes an implosion - if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) { - ev.stopPropagation(); - return; - } - if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { // synchronous dispatch so we focus before key generates input dis.dispatch({action: 'focus_composer'}, true); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0449c2b4fd..0361cdaeb9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2017-2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -20,7 +20,8 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import Matrix from "matrix-js-sdk"; +import * as Matrix from "matrix-js-sdk"; +import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto'; // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss import 'focus-visible'; @@ -29,7 +30,7 @@ import 'what-input'; import Analytics from "../../Analytics"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; -import MatrixClientPeg from "../../MatrixClientPeg"; +import {MatrixClientPeg} from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; import SdkConfig from "../../SdkConfig"; import * as RoomListSorter from "../../RoomListSorter"; @@ -38,7 +39,7 @@ import Notifier from '../../Notifier'; import Modal from "../../Modal"; import Tinter from "../../Tinter"; -import sdk from '../../index'; +import * as sdk from '../../index'; import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite'; import * as Rooms from '../../Rooms'; import linkifyMatrix from "../../linkify-matrix"; @@ -64,7 +65,7 @@ import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; /** constants for MatrixChat.state.view */ -const VIEWS = { +export const VIEWS = { // a special initial state which is only used at startup, while we are // trying to re-animate a matrix client or register as a guest. LOADING: 0, @@ -78,18 +79,14 @@ const VIEWS = { // we are showing the registration view REGISTER: 3, - // completeing the registration flow + // completing the registration flow POST_REGISTRATION: 4, // showing the 'forgot password' view FORGOT_PASSWORD: 5, - // we have valid matrix credentials (either via an explicit login, via the - // initial re-animation/guest registration, or via a registration), and are - // now setting up a matrixclient to talk to it. This isn't an instant - // process because we need to clear out indexeddb. While it is going on we - // show a big spinner. - LOGGING_IN: 6, + // showing flow to trust this new device with cross-signing + COMPLETE_SECURITY: 6, // we are logged in with an active matrix client. LOGGED_IN: 7, @@ -655,16 +652,12 @@ export default createReactClass({ }); break; } - case 'on_logging_in': - // We are now logging in, so set the state to reflect that - // NB. This does not touch 'ready' since if our dispatches - // are delayed, the sync could already have completed - this.setStateForNewView({ - view: VIEWS.LOGGING_IN, - }); - break; case 'on_logged_in': - if (!Lifecycle.isSoftLogout()) { + if ( + !Lifecycle.isSoftLogout() && + this.state.view !== VIEWS.LOGIN && + this.state.view !== VIEWS.COMPLETE_SECURITY + ) { this._onLoggedIn(); } break; @@ -1168,7 +1161,7 @@ export default createReactClass({ if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { const welcomeUserRoom = await this._startWelcomeUserChat(); if (welcomeUserRoom === null) { - // We didn't rediret to the welcome user room, so show + // We didn't redirect to the welcome user room, so show // the homepage. dis.dispatch({action: 'view_home_page'}); } @@ -1500,6 +1493,15 @@ export default createReactClass({ "blacklistUnverifiedDevices", ); cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled); + + // With cross-signing enabled, we send to unknown devices + // without prompting. Any bad-device status the user should + // be aware of will be signalled through the room shield + // changing colour. More advanced behaviour will come once + // we implement more settings. + cli.setGlobalErrorOnUnknownDevices( + !SettingsStore.isFeatureEnabled("feature_cross_signing"), + ); } }, @@ -1559,6 +1561,10 @@ export default createReactClass({ dis.dispatch({ action: 'view_my_groups', }); + } else if (screen === 'complete_security') { + dis.dispatch({ + action: 'start_complete_security', + }); } else if (screen == 'post_registration') { dis.dispatch({ action: 'start_post_registration', @@ -1808,21 +1814,69 @@ export default createReactClass({ this._loggedInView = ref; }, + async onUserCompletedLoginFlow(credentials) { + // Wait for the client to be logged in (but not started) + // which is enough to ask the server about account data. + const loggedIn = new Promise(resolve => { + const actionHandlerRef = dis.register(payload => { + if (payload.action !== "on_logged_in") { + return; + } + dis.unregister(actionHandlerRef); + resolve(); + }); + }); + + // Create and start the client in the background + Lifecycle.setLoggedIn(credentials); + await loggedIn; + + const cli = MatrixClientPeg.get(); + // We're checking `isCryptoAvailable` here instead of `isCryptoEnabled` + // because the client hasn't been started yet. + if (!isCryptoAvailable()) { + this._onLoggedIn(); + } + + // Test for the master cross-signing key in SSSS as a quick proxy for + // whether cross-signing has been set up on the account. + let masterKeyInStorage = false; + try { + masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master"); + } catch (e) { + if (e.errcode !== "M_NOT_FOUND") throw e; + } + + if (masterKeyInStorage) { + this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY }); + } else { + this._onLoggedIn(); + } + }, + + onCompleteSecurityFinished() { + this._onLoggedIn(); + }, + render: function() { // console.log(`Rendering MatrixChat with view ${this.state.view}`); let view; - if ( - this.state.view === VIEWS.LOADING || - this.state.view === VIEWS.LOGGING_IN - ) { + if (this.state.view === VIEWS.LOADING) { const Spinner = sdk.getComponent('elements.Spinner'); view = (
); + } else if (this.state.view === VIEWS.COMPLETE_SECURITY) { + const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity'); + view = ( + + ); } else if (this.state.view === VIEWS.POST_REGISTRATION) { // needs to be before normal PageTypes as you are logged in technically const PostRegistration = sdk.getComponent('structures.auth.PostRegistration'); @@ -1907,7 +1961,7 @@ export default createReactClass({ const Login = sdk.getComponent('structures.auth.Login'); view = ( ; - } else { - const SlateMessageComposer = sdk.getComponent('rooms.SlateMessageComposer'); - messageComposer = - ; - } + const MessageComposer = sdk.getComponent('rooms.MessageComposer'); + messageComposer = + ; } // TODO: Why aren't we storing the term/scope/count in this format @@ -2013,5 +1992,3 @@ module.exports = createReactClass({ ); }, }); - -module.exports.RoomContext = RoomContext; diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index f289720542..bc7c400949 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -84,7 +84,7 @@ if (DEBUG_SCROLL) { * offset as normal. */ -module.exports = createReactClass({ +export default createReactClass({ displayName: 'ScrollPanel', propTypes: { diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 9090152de8..3be2f65dc5 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -24,7 +24,7 @@ import { throttle } from 'lodash'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; import classNames from 'classnames'; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'SearchBox', propTypes: { diff --git a/src/components/structures/TabbedView.js b/src/components/structures/TabbedView.js index 01c68fad62..20af183af8 100644 --- a/src/components/structures/TabbedView.js +++ b/src/components/structures/TabbedView.js @@ -19,7 +19,7 @@ limitations under the License. import * as React from "react"; import {_t} from '../../languageHandler'; import PropTypes from "prop-types"; -import sdk from "../../index"; +import * as sdk from "../../index"; /** * Represents a tab for the TabbedView. @@ -38,7 +38,7 @@ export class Tab { } } -export class TabbedView extends React.Component { +export default class TabbedView extends React.Component { static propTypes = { // The tabs to show tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired, diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index c32da7193e..cefb60653f 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -20,7 +20,7 @@ import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; -import sdk from '../../index'; +import * as sdk from '../../index'; import dis from '../../dispatcher'; import { _t } from '../../languageHandler'; diff --git a/src/components/structures/TagPanelButtons.js b/src/components/structures/TagPanelButtons.js new file mode 100644 index 0000000000..93a596baa3 --- /dev/null +++ b/src/components/structures/TagPanelButtons.js @@ -0,0 +1,59 @@ +/* +Copyright 2019 New Vector Ltd. + +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 createReactClass from 'create-react-class'; +import * as sdk from '../../index'; +import dis from '../../dispatcher'; +import Modal from '../../Modal'; +import { _t } from '../../languageHandler'; + +const TagPanelButtons = createReactClass({ + displayName: 'TagPanelButtons', + + + componentDidMount: function() { + this._dispatcherRef = dis.register(this._onAction); + }, + + componentWillUnmount() { + if (this._dispatcherRef) { + dis.unregister(this._dispatcherRef); + this._dispatcherRef = null; + } + }, + + _onAction(payload) { + if (payload.action === "show_redesign_feedback_dialog") { + const RedesignFeedbackDialog = + sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); + Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); + } + }, + + render() { + const GroupsButton = sdk.getComponent('elements.GroupsButton'); + const ActionButton = sdk.getComponent("elements.ActionButton"); + + return (
+ + +
); + }, +}); +export default TagPanelButtons; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 53ae240653..a3ebd31d61 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -18,26 +18,24 @@ limitations under the License. */ import SettingsStore from "../../settings/SettingsStore"; - import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; - -const Matrix = require("matrix-js-sdk"); -const EventTimeline = Matrix.EventTimeline; - -const sdk = require('../../index'); +import {EventTimeline} from "matrix-js-sdk"; +import * as Matrix from "matrix-js-sdk"; import { _t } from '../../languageHandler'; -const MatrixClientPeg = require("../../MatrixClientPeg"); -const dis = require("../../dispatcher"); -const ObjectUtils = require('../../ObjectUtils'); -const Modal = require("../../Modal"); -const UserActivity = require("../../UserActivity"); -import {Key} from '../../Keyboard'; +import {MatrixClientPeg} from "../../MatrixClientPeg"; +import * as ObjectUtils from "../../ObjectUtils"; +import UserActivity from "../../UserActivity"; +import Modal from "../../Modal"; +import dis from "../../dispatcher"; +import * as sdk from "../../index"; +import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; +import {haveTileForEvent} from "../views/rooms/EventTile"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -1138,8 +1136,6 @@ const TimelinePanel = createReactClass({ const messagePanel = this._messagePanel.current; if (!messagePanel) return null; - const EventTile = sdk.getComponent('rooms.EventTile'); - const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect(); const myUserId = MatrixClientPeg.get().credentials.userId; @@ -1181,7 +1177,7 @@ const TimelinePanel = createReactClass({ const shouldIgnore = !!ev.status || // local echo (ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message - const isWithoutTile = !EventTile.haveTileForEvent(ev) || shouldHideEvent(ev); + const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev); if (isWithoutTile || !node) { // don't start counting if the event should be ignored, @@ -1346,4 +1342,4 @@ const TimelinePanel = createReactClass({ }, }); -module.exports = TimelinePanel; +export default TimelinePanel; diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js index e7928ab4d7..967805d099 100644 --- a/src/components/structures/TopLeftMenuButton.js +++ b/src/components/structures/TopLeftMenuButton.js @@ -19,8 +19,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import {TopLeftMenu} from '../views/context_menus/TopLeftMenu'; import BaseAvatar from '../views/avatars/BaseAvatar'; -import MatrixClientPeg from '../../MatrixClientPeg'; -import Avatar from '../../Avatar'; +import {MatrixClientPeg} from '../../MatrixClientPeg'; +import * as Avatar from '../../Avatar'; import { _t } from '../../languageHandler'; import dis from "../../dispatcher"; import {ContextMenu, ContextMenuButton} from "./ContextMenu"; diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index da0ca7fe99..1aec63f04e 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 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. @@ -18,11 +19,11 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import ContentMessages from '../../ContentMessages'; -const dis = require('../../dispatcher'); -const filesize = require('filesize'); +import dis from "../../dispatcher"; +import filesize from "filesize"; import { _t } from '../../languageHandler'; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'UploadBar', propTypes: { room: PropTypes.object, diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js index 26d0ff5044..94159a1da4 100644 --- a/src/components/structures/UserView.js +++ b/src/components/structures/UserView.js @@ -18,8 +18,8 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; import Matrix from "matrix-js-sdk"; -import MatrixClientPeg from "../../MatrixClientPeg"; -import sdk from "../../index"; +import {MatrixClientPeg} from "../../MatrixClientPeg"; +import * as sdk from "../../index"; import Modal from '../../Modal'; import { _t } from '../../languageHandler'; diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index ef4ede517a..326ba2c22f 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 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. @@ -20,10 +21,10 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import SyntaxHighlight from '../views/elements/SyntaxHighlight'; import {_t} from "../../languageHandler"; -import sdk from "../../index"; +import * as sdk from "../../index"; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'ViewSource', propTypes: { diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js new file mode 100644 index 0000000000..77f7fe26e4 --- /dev/null +++ b/src/components/structures/auth/CompleteSecurity.js @@ -0,0 +1,173 @@ +/* +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 { _t } from '../../../languageHandler'; +import * as sdk from '../../../index'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { accessSecretStorage } from '../../../CrossSigningManager'; + +const PHASE_INTRO = 0; +const PHASE_DONE = 1; +const PHASE_CONFIRM_SKIP = 2; + +export default class CompleteSecurity extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + constructor() { + super(); + + this.state = { + phase: PHASE_INTRO, + }; + } + + onStartClick = async () => { + const cli = MatrixClientPeg.get(); + await accessSecretStorage(async () => { + await cli.checkOwnCrossSigningTrust(); + }); + this.setState({ + phase: PHASE_DONE, + }); + } + + onSkipClick = () => { + this.setState({ + phase: PHASE_CONFIRM_SKIP, + }); + } + + onSkipConfirmClick = () => { + this.props.onFinished(); + } + + onSkipBackClick = () => { + this.setState({ + phase: PHASE_INTRO, + }); + } + + onDoneClick = () => { + this.props.onFinished(); + } + + render() { + const AuthPage = sdk.getComponent("auth.AuthPage"); + const AuthHeader = sdk.getComponent("auth.AuthHeader"); + const AuthBody = sdk.getComponent("auth.AuthBody"); + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + const { + phase, + } = this.state; + + let icon; + let title; + let body; + if (phase === PHASE_INTRO) { + icon = ; + title = _t("Complete security"); + body = ( +
+

{_t( + "Verify this session to grant it access to encrypted messages.", + )}

+
+ + {_t("Skip")} + + + {_t("Start")} + +
+
+ ); + } else if (phase === PHASE_DONE) { + icon = ; + title = _t("Session verified"); + body = ( +
+
+

{_t( + "Your new session is now verified. It has access to your " + + "encrypted messages, and other users will see it as trusted.", + )}

+
+ + {_t("Done")} + +
+
+ ); + } else if (phase === PHASE_CONFIRM_SKIP) { + icon = ; + title = _t("Are you sure?"); + body = ( +
+

{_t( + "Without completing security on this device, it won’t have " + + "access to encrypted messages.", + )}

+
+ + {_t("Skip")} + + + {_t("Go Back")} + +
+
+ ); + } else { + throw new Error(`Unknown phase ${phase}`); + } + + return ( + + + +

+ {icon} + {title} +

+
+ {body} +
+
+
+ ); + } +} diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 6f68293caa..4576067caa 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -20,12 +20,13 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import Modal from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; import PasswordReset from "../../../PasswordReset"; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from 'classnames'; +import AuthPage from "../../views/auth/AuthPage"; // Phases // Show controls to configure server details @@ -39,7 +40,7 @@ const PHASE_EMAIL_SENT = 3; // User has clicked the link in email and completed reset const PHASE_DONE = 4; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'ForgotPassword', propTypes: { @@ -367,7 +368,6 @@ module.exports = createReactClass({ }, render: function() { - const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index b2e9d3e7cd..7bc2dbcbae 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -20,12 +20,13 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import {_t, _td} from '../../../languageHandler'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; +import AuthPage from "../../views/auth/AuthPage"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -53,7 +54,7 @@ _td("General failure"); /** * A wire component which glues together login UI components and Login logic */ -module.exports = createReactClass({ +export default createReactClass({ displayName: 'Login', propTypes: { @@ -608,7 +609,6 @@ module.exports = createReactClass({ render: function() { const Loader = sdk.getComponent("elements.Spinner"); - const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); const loader = this.isBusy() ?
: null; diff --git a/src/components/structures/auth/PostRegistration.js b/src/components/structures/auth/PostRegistration.js index 760163585d..8eef8dce11 100644 --- a/src/components/structures/auth/PostRegistration.js +++ b/src/components/structures/auth/PostRegistration.js @@ -17,11 +17,12 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import * as sdk from '../../../index'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; +import AuthPage from "../../views/auth/AuthPage"; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'PostRegistration', propTypes: { @@ -59,7 +60,6 @@ module.exports = createReactClass({ render: function() { const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); - const AuthPage = sdk.getComponent('auth.AuthPage'); const AuthHeader = sdk.getComponent('auth.AuthHeader'); const AuthBody = sdk.getComponent("auth.AuthBody"); return ( diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 3578d745f5..fdf2f51e00 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -21,7 +21,7 @@ import Matrix from 'matrix-js-sdk'; import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; @@ -29,7 +29,8 @@ import * as ServerType from '../../views/auth/ServerTypeSelector'; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; import * as Lifecycle from '../../../Lifecycle'; -import MatrixClientPeg from "../../../MatrixClientPeg"; +import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import AuthPage from "../../views/auth/AuthPage"; // Phases // Show controls to configure server details @@ -40,7 +41,7 @@ const PHASE_REGISTRATION = 1; // Enable phases for registration const PHASES_ENABLED = true; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'Registration', propTypes: { @@ -576,7 +577,6 @@ module.exports = createReactClass({ render: function() { const AuthHeader = sdk.getComponent('auth.AuthHeader'); const AuthBody = sdk.getComponent("auth.AuthBody"); - const AuthPage = sdk.getComponent('auth.AuthPage'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let errorText; diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 585b4bfe67..40800ad907 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -17,13 +17,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {_t} from '../../../languageHandler'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import dis from '../../../dispatcher'; import * as Lifecycle from '../../../Lifecycle'; import Modal from '../../../Modal'; -import MatrixClientPeg from "../../../MatrixClientPeg"; +import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {sendLoginRequest} from "../../../Login"; import url from 'url'; +import AuthPage from "../../views/auth/AuthPage"; const LOGIN_VIEW = { LOADING: 1, @@ -65,7 +66,7 @@ export default class SoftLogout extends React.Component { componentDidMount(): void { // We've ended up here when we don't need to - navigate to login if (!Lifecycle.isSoftLogout()) { - dis.dispatch({action: "on_logged_in"}); + dis.dispatch({action: "start_login"}); return; } @@ -284,7 +285,6 @@ export default class SoftLogout extends React.Component { } render() { - const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.js index 39d636f9cc..4076141606 100644 --- a/src/components/views/auth/AuthFooter.js +++ b/src/components/views/auth/AuthFooter.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 New Vector Ltd +Copyright 2019 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. @@ -19,7 +20,7 @@ import { _t } from '../../../languageHandler'; import React from 'react'; import createReactClass from 'create-react-class'; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'AuthFooter', render: function() { diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.js index 193f347857..133fd41359 100644 --- a/src/components/views/auth/AuthHeader.js +++ b/src/components/views/auth/AuthHeader.js @@ -17,9 +17,9 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'AuthHeader', render: function() { diff --git a/src/components/views/auth/AuthPage.js b/src/components/views/auth/AuthPage.js index 41098c9d6c..82f7270121 100644 --- a/src/components/views/auth/AuthPage.js +++ b/src/components/views/auth/AuthPage.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 New Vector Ltd +Copyright 2019 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. @@ -16,22 +17,21 @@ limitations under the License. */ import React from 'react'; -import createReactClass from 'create-react-class'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; +import {replaceableComponent} from "../../../utils/replaceableComponent"; -module.exports = createReactClass({ - displayName: 'AuthPage', - - render: function() { +@replaceableComponent("views.auth.AuthPage") +export default class AuthPage extends React.PureComponent { + render() { const AuthFooter = sdk.getComponent('auth.AuthFooter'); return (
- { this.props.children } + {this.props.children}
); - }, -}); + } +} diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js index f907a58026..2da837f029 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.js @@ -24,7 +24,7 @@ const DIV_ID = 'mx_recaptcha'; /** * A pure UI component which displays a captcha form. */ -module.exports = createReactClass({ +export default createReactClass({ displayName: 'CaptchaForm', propTypes: { diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.js index 567bcf59ef..63dc9d1ada 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { COUNTRIES } from '../../../phonenumber'; import SdkConfig from "../../../SdkConfig"; diff --git a/src/components/views/auth/CustomServerDialog.js b/src/components/views/auth/CustomServerDialog.js index a9a3a53f02..024951e6c0 100644 --- a/src/components/views/auth/CustomServerDialog.js +++ b/src/components/views/auth/CustomServerDialog.js @@ -19,7 +19,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'CustomServerDialog', render: function() { diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index dd661291f3..869e81c1f7 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -22,7 +22,7 @@ import PropTypes from 'prop-types'; import url from 'url'; import classnames from 'classnames'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/components/views/auth/LanguageSelector.js b/src/components/views/auth/LanguageSelector.js index 32862478f4..99578d4504 100644 --- a/src/components/views/auth/LanguageSelector.js +++ b/src/components/views/auth/LanguageSelector.js @@ -18,7 +18,7 @@ import SdkConfig from "../../../SdkConfig"; import {getCurrentLanguage} from "../../../languageHandler"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import PlatformPeg from "../../../PlatformPeg"; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import React from 'react'; function onChange(newLang) { diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index 684e8d5912..32418d3462 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import SdkConfig from "../../../SdkConfig"; diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 63e77a938d..c836b96a89 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -19,7 +19,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 03fb74462c..91f8e1b226 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -20,8 +20,8 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; -import Email from '../../../email'; +import * as sdk from '../../../index'; +import * as Email from '../../../email'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; @@ -41,7 +41,7 @@ const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from of /** * A pure UI component which displays a registration form. */ -module.exports = createReactClass({ +export default createReactClass({ displayName: 'RegistrationForm', propTypes: { diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index e5523b1e36..5e17d50b55 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -19,12 +19,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import Modal from '../../../Modal'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import SdkConfig from "../../../SdkConfig"; -import { createClient } from 'matrix-js-sdk/lib/matrix'; +import { createClient } from 'matrix-js-sdk/src/matrix'; import classNames from 'classnames'; /* diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js index ebc2ea6d37..341f81c546 100644 --- a/src/components/views/auth/ServerTypeSelector.js +++ b/src/components/views/auth/ServerTypeSelector.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import classnames from 'classnames'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import {makeType} from "../../../utils/TypeUtils"; diff --git a/src/components/views/auth/SignInToText.js b/src/components/views/auth/SignInToText.js index a7acdc6705..7564096b7d 100644 --- a/src/components/views/auth/SignInToText.js +++ b/src/components/views/auth/SignInToText.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import {_t} from "../../../languageHandler"; -import sdk from "../../../index"; +import * as sdk from "../../../index"; import PropTypes from "prop-types"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js index 9cc398329d..58f117ea36 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.js @@ -15,12 +15,12 @@ limitations under the License. */ import React from 'react'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; +import AuthPage from "./AuthPage"; export default class Welcome extends React.PureComponent { render() { - const AuthPage = sdk.getComponent("auth.AuthPage"); const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); const LanguageSelector = sdk.getComponent('auth.LanguageSelector'); diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 59b509889b..4c34cee853 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 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. @@ -19,12 +20,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import AvatarLogic from '../../../Avatar'; +import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'BaseAvatar', propTypes: { diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.js index e8ef2a5279..0da57bcb99 100644 --- a/src/components/views/avatars/GroupAvatar.js +++ b/src/components/views/avatars/GroupAvatar.js @@ -17,8 +17,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import * as sdk from '../../../index'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; export default createReactClass({ displayName: 'GroupAvatar', diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index 383bab5e79..a07a184aa1 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 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. @@ -17,11 +18,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -const Avatar = require('../../../Avatar'); -const sdk = require("../../../index"); -const dispatcher = require("../../../dispatcher"); +import * as Avatar from '../../../Avatar'; +import * as sdk from "../../../index"; +import dis from "../../../dispatcher"; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'MemberAvatar', propTypes: { @@ -55,7 +56,7 @@ module.exports = createReactClass({ }, _getState: function(props) { - if (props.member) { + if (props.member && props.member.name) { return { name: props.member.name, title: props.title || props.member.userId, @@ -82,7 +83,7 @@ module.exports = createReactClass({ if (viewUserOnClick) { onClick = () => { - dispatcher.dispatch({ + dis.dispatch({ action: 'view_user', member: this.props.member, }); diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 245d869419..aaac61ce7d 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -16,7 +16,7 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {_t} from "../../../languageHandler"; import MemberAvatar from '../avatars/MemberAvatar'; import classNames from 'classnames'; diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index 6f8f236afc..c79e1827da 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -16,13 +16,13 @@ limitations under the License. import React from "react"; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import {ContentRepo} from "matrix-js-sdk"; -import MatrixClientPeg from "../../../MatrixClientPeg"; +import {MatrixClientPeg} from "../../../MatrixClientPeg"; import Modal from '../../../Modal'; -import sdk from "../../../index"; -import Avatar from '../../../Avatar'; +import * as sdk from "../../../index"; +import * as Avatar from '../../../Avatar'; +import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'RoomAvatar', // Room may be left unset here, but if it is, @@ -82,7 +82,7 @@ module.exports = createReactClass({ getImageUrls: function(props) { return [ - ContentRepo.getHttpUriForMxc( + getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), props.oobData.avatarUrl, Math.floor(props.width * window.devicePixelRatio), diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js index 3c0fd081b4..27ef76452f 100644 --- a/src/components/views/context_menus/GroupInviteTileContextMenu.js +++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import {Group} from 'matrix-js-sdk'; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index efbfc4322f..7215a45be2 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -22,9 +22,9 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import {EventStatus} from 'matrix-js-sdk'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import Resend from '../../../Resend'; @@ -37,7 +37,7 @@ function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } -module.exports = createReactClass({ +export default createReactClass({ displayName: 'MessageContextMenu', propTypes: { @@ -422,7 +422,7 @@ module.exports = createReactClass({ ); - if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) { + if (this.props.eventTileOps) { // this event is rendered using TextualBody quoteButton = ( { _t('Quote') } diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index f5e68bd20b..6e2bd8ebf5 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -21,9 +21,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; import DMRoomMap from '../../../utils/DMRoomMap'; import * as Rooms from '../../../Rooms'; @@ -63,7 +63,7 @@ const NotifOption = ({active, onClick, src, label}) => { ); }; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'RoomTileContextMenu', propTypes: { diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index 31ba788ec7..d5cba45956 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -17,8 +17,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; -import sdk from '../../../index'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import * as sdk from '../../../index'; import AccessibleButton from '../elements/AccessibleButton'; export default class StatusMessageContextMenu extends React.Component { diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index 388d8aaf3d..7313a278cc 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; import TagOrderActions from '../../../actions/TagOrderActions'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import {MenuItem} from "../../structures/ContextMenu"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index 08a65e2f21..528e4790c2 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -23,9 +23,9 @@ import LogoutDialog from "../dialogs/LogoutDialog"; import Modal from "../../../Modal"; import SdkConfig from '../../../SdkConfig'; import { getHostingLink } from '../../../utils/HostingLink'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MenuItem} from "../../structures/ContextMenu"; -import sdk from "../../../index"; +import * as sdk from "../../../index"; export class TopLeftMenu extends React.Component { static propTypes = { diff --git a/src/components/views/create_room/CreateRoomButton.js b/src/components/views/create_room/CreateRoomButton.js index 1c44aed78c..adf3972eff 100644 --- a/src/components/views/create_room/CreateRoomButton.js +++ b/src/components/views/create_room/CreateRoomButton.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 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. @@ -19,7 +20,7 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'CreateRoomButton', propTypes: { onCreateRoom: PropTypes.func, diff --git a/src/components/views/create_room/Presets.js b/src/components/views/create_room/Presets.js index f512c3e2fd..0f18d11511 100644 --- a/src/components/views/create_room/Presets.js +++ b/src/components/views/create_room/Presets.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 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. @@ -25,7 +26,7 @@ const Presets = { Custom: "custom", }; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'CreateRoomPresets', propTypes: { onChange: PropTypes.func, diff --git a/src/components/views/create_room/RoomAlias.js b/src/components/views/create_room/RoomAlias.js index fd3e3365f7..bc5dec1468 100644 --- a/src/components/views/create_room/RoomAlias.js +++ b/src/components/views/create_room/RoomAlias.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 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. @@ -19,7 +20,7 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; -module.exports = createReactClass({ +export default createReactClass({ displayName: 'RoomAlias', propTypes: { // Specifying a homeserver will make magical things happen when you, diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index f32cc40a96..e309c3a0cf 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -22,8 +22,8 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import { _t, _td } from '../../../languageHandler'; -import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import * as sdk from '../../../index'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; @@ -44,7 +44,7 @@ const addressTypeName = { }; -module.exports = createReactClass({ +export default createReactClass({ displayName: "AddressPickerDialog", propTypes: { diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js index 3d10752ff8..7fa6069478 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index a9f7fbf4b3..19f22a15ad 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -24,7 +24,7 @@ import classNames from 'classnames'; import { Key } from '../../../Keyboard'; import AccessibleButton from '../elements/AccessibleButton'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js index 91d2bb5213..ccb332fa60 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.js @@ -19,7 +19,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js index 91f1af64e3..e58f56a639 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.js @@ -17,7 +17,7 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> import React from 'react'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import request from 'browser-request'; import { _t } from '../../../languageHandler'; diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js index db00f445a8..0622dd7dfb 100644 --- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js +++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; /* diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js index c606706ed2..71139155ec 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.js @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; /* diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 4d33b2b500..14910fbf6d 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -18,7 +18,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js index b7ad5c2557..49b558976a 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; -import sdk from "../../../index"; +import * as sdk from "../../../index"; export default class ConfirmWipeDeviceDialog extends React.Component { static propTypes = { diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 3430a12e71..d465ef26a2 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -17,10 +17,10 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; export default createReactClass({ displayName: 'CreateGroupDialog', diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index 5ddebb1119..288074a891 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -17,11 +17,11 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import withValidation from '../elements/Validation'; import { _t } from '../../../languageHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {Key} from "../../../Keyboard"; export default createReactClass({ diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.js b/src/components/views/dialogs/CryptoStoreTooNewDialog.js index 0146420f46..11e202b0cc 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.js +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.js @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import sdk from '../../../index'; +import * as sdk from '../../../index'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js new file mode 100644 index 0000000000..c0ff9b96fe --- /dev/null +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -0,0 +1,777 @@ +/* +Copyright 2019, 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, {createRef} from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import * as sdk from "../../../index"; +import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import {makeUserPermalink} from "../../../utils/permalinks/Permalinks"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {RoomMember} from "matrix-js-sdk/src/matrix"; +import SdkConfig from "../../../SdkConfig"; +import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; +import * as Email from "../../../email"; +import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils"; +import {abbreviateUrl} from "../../../utils/UrlUtils"; +import dis from "../../../dispatcher"; +import IdentityAuthClient from "../../../IdentityAuthClient"; +import Modal from "../../../Modal"; +import {humanizeTime} from "../../../utils/humanize"; + +// TODO: [TravisR] Make this generic for all kinds of invites + +const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first +const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked + +// This is the interface that is expected by various components in this file. It is a bit +// awkward because it also matches the RoomMember class from the js-sdk with some extra support +// for 3PIDs/email addresses. +// +// XXX: We should use TypeScript interfaces instead of this weird "abstract" class. +class Member { + /** + * The display name of this Member. For users this should be their profile's display + * name or user ID if none set. For 3PIDs this should be the 3PID address (email). + */ + get name(): string { throw new Error("Member class not implemented"); } + + /** + * The ID of this Member. For users this should be their user ID. For 3PIDs this should + * be the 3PID address (email). + */ + get userId(): string { throw new Error("Member class not implemented"); } + + /** + * Gets the MXC URL of this Member's avatar. For users this should be their profile's + * avatar MXC URL or null if none set. For 3PIDs this should always be null. + */ + getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); } +} + +class DirectoryMember extends Member { + _userId: string; + _displayName: string; + _avatarUrl: string; + + constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) { + super(); + this._userId = userDirResult.user_id; + this._displayName = userDirResult.display_name; + this._avatarUrl = userDirResult.avatar_url; + } + + // These next class members are for the Member interface + get name(): string { + return this._displayName || this._userId; + } + + get userId(): string { + return this._userId; + } + + getMxcAvatarUrl(): string { + return this._avatarUrl; + } +} + +class ThreepidMember extends Member { + _id: string; + + constructor(id: string) { + super(); + this._id = id; + } + + // This is a getter that would be falsey on all other implementations. Until we have + // better type support in the react-sdk we can use this trick to determine the kind + // of 3PID we're dealing with, if any. + get isEmail(): boolean { + return this._id.includes('@'); + } + + // These next class members are for the Member interface + get name(): string { + return this._id; + } + + get userId(): string { + return this._id; + } + + getMxcAvatarUrl(): string { + return null; + } +} + +class DMUserTile extends React.PureComponent { + static propTypes = { + member: PropTypes.object.isRequired, // Should be a Member (see interface above) + onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed + }; + + _onRemove = (e) => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + + this.props.onRemove(this.props.member); + }; + + render() { + const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + const avatarSize = 20; + const avatar = this.props.member.isEmail + ? + : ; + + return ( + + + {avatar} + {this.props.member.name} + + + {_t('Remove')} + + + ); + } +} + +class DMRoomTile extends React.PureComponent { + static propTypes = { + member: PropTypes.object.isRequired, // Should be a Member (see interface above) + lastActiveTs: PropTypes.number, + onToggle: PropTypes.func.isRequired, // takes 1 argument, the member being toggled + highlightWord: PropTypes.string, + isSelected: PropTypes.bool, + }; + + _onClick = (e) => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + + this.props.onToggle(this.props.member); + }; + + _highlightName(str: string) { + if (!this.props.highlightWord) return str; + + // We convert things to lowercase for index searching, but pull substrings from + // the submitted text to preserve case. Note: we don't need to htmlEntities the + // string because React will safely encode the text for us. + const lowerStr = str.toLowerCase(); + const filterStr = this.props.highlightWord.toLowerCase(); + + const result = []; + + let i = 0; + let ii; + while ((ii = lowerStr.indexOf(filterStr, i)) >= 0) { + // Push any text we missed (first bit/middle of text) + if (ii > i) { + // Push any text we aren't highlighting (middle of text match, or beginning of text) + result.push({str.substring(i, ii)}); + } + + i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching) + + // Highlight the word the user entered + const substr = str.substring(i, filterStr.length + i); + result.push({substr}); + i += substr.length; + } + + // Push any text we missed (end of text) + if (i < (str.length - 1)) { + result.push({str.substring(i)}); + } + + return result; + } + + render() { + const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); + + let timestamp = null; + if (this.props.lastActiveTs) { + const humanTs = humanizeTime(this.props.lastActiveTs); + timestamp = {humanTs}; + } + + const avatarSize = 36; + const avatar = this.props.member.isEmail + ? + : ; + + let checkmark = null; + if (this.props.isSelected) { + // To reduce flickering we put the 'selected' room tile above the real avatar + checkmark =
; + } + + // To reduce flickering we put the checkmark on top of the actual avatar (prevents + // the browser from reloading the image source when the avatar remounts). + const stackedAvatar = ( + + {avatar} + {checkmark} + + ); + + return ( +
+ {stackedAvatar} + {this._highlightName(this.props.member.name)} + {this._highlightName(this.props.member.userId)} + {timestamp} +
+ ); + } +} + +export default class DMInviteDialog extends React.PureComponent { + static propTypes = { + // Takes an array of user IDs/emails to invite. + onFinished: PropTypes.func.isRequired, + }; + + _debounceTimer: number = null; + _editorRef: any = null; + + constructor() { + super(); + + this.state = { + targets: [], // array of Member objects (see interface above) + filterText: "", + recents: this._buildRecents(), + numRecentsShown: INITIAL_ROOMS_SHOWN, + suggestions: this._buildSuggestions(), + numSuggestionsShown: INITIAL_ROOMS_SHOWN, + serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions + threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions + canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(), + tryingIdentityServer: false, + }; + + this._editorRef = createRef(); + } + + _buildRecents(): {userId: string, user: RoomMember, lastActive: number} { + const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); + const recents = []; + for (const userId in rooms) { + const room = rooms[userId]; + const member = room.getMember(userId); + if (!member) continue; // just skip people who don't have memberships for some reason + + const lastEventTs = room.timeline && room.timeline.length + ? room.timeline[room.timeline.length - 1].getTs() + : 0; + if (!lastEventTs) continue; // something weird is going on with this room + + recents.push({userId, user: member, lastActive: lastEventTs}); + } + + // Sort the recents by last active to save us time later + recents.sort((a, b) => b.lastActive - a.lastActive); + + return recents; + } + + _buildSuggestions(): {userId: string, user: RoomMember} { + const maxConsideredMembers = 200; + const client = MatrixClientPeg.get(); + const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']]; + const joinedRooms = client.getRooms() + .filter(r => r.getMyMembership() === 'join') + .filter(r => r.getJoinedMemberCount() <= maxConsideredMembers); + + // Generates { userId: {member, rooms[]} } + const memberRooms = joinedRooms.reduce((members, room) => { + // Filter out DMs (we'll handle these in the recents section) + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + return members; // Do nothing + } + + const joinedMembers = room.getJoinedMembers().filter(u => !excludedUserIds.includes(u.userId)); + for (const member of joinedMembers) { + if (!members[member.userId]) { + members[member.userId] = { + member: member, + // Track the room size of the 'picked' member so we can use the profile of + // the smallest room (likely a DM). + pickedMemberRoomSize: room.getJoinedMemberCount(), + rooms: [], + }; + } + + members[member.userId].rooms.push(room); + + if (room.getJoinedMemberCount() < members[member.userId].pickedMemberRoomSize) { + members[member.userId].member = member; + members[member.userId].pickedMemberRoomSize = room.getJoinedMemberCount(); + } + } + return members; + }, {}); + + // Generates { userId: {member, numRooms, score} } + const memberScores = Object.values(memberRooms).reduce((scores, entry) => { + const numMembersTotal = entry.rooms.reduce((c, r) => c + r.getJoinedMemberCount(), 0); + const maxRange = maxConsideredMembers * entry.rooms.length; + scores[entry.member.userId] = { + member: entry.member, + numRooms: entry.rooms.length, + score: Math.max(0, Math.pow(1 - (numMembersTotal / maxRange), 5)), + }; + return scores; + }, {}); + + const members = Object.values(memberScores); + members.sort((a, b) => { + if (a.score === b.score) { + if (a.numRooms === b.numRooms) { + return a.member.userId.localeCompare(b.member.userId); + } + + return b.numRooms - a.numRooms; + } + return b.score - a.score; + }); + + return members.map(m => ({userId: m.member.userId, user: m.member})); + } + + _startDm = () => { + this.props.onFinished(this.state.targets.map(t => t.userId)); + }; + + _cancel = () => { + this.props.onFinished([]); + }; + + _updateFilter = (e) => { + const term = e.target.value; + this.setState({filterText: term}); + + // Debounce server lookups to reduce spam. We don't clear the existing server + // results because they might still be vaguely accurate, likewise for races which + // could happen here. + if (this._debounceTimer) { + clearTimeout(this._debounceTimer); + } + this._debounceTimer = setTimeout(async () => { + MatrixClientPeg.get().searchUserDirectory({term}).then(r => { + if (term !== this.state.filterText) { + // Discard the results - we were probably too slow on the server-side to make + // these results useful. This is a race we want to avoid because we could overwrite + // more accurate results. + return; + } + this.setState({ + serverResultsMixin: r.results.map(u => ({ + userId: u.user_id, + user: new DirectoryMember(u), + })), + }); + }).catch(e => { + console.error("Error searching user directory:"); + console.error(e); + this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal + }); + + // Whenever we search the directory, also try to search the identity server. It's + // all debounced the same anyways. + if (!this.state.canUseIdentityServer) { + // The user doesn't have an identity server set - warn them of that. + this.setState({tryingIdentityServer: true}); + return; + } + if (term.indexOf('@') > 0 && Email.looksValid(term)) { + // Start off by suggesting the plain email while we try and resolve it + // to a real account. + this.setState({ + // per above: the userId is a lie here - it's just a regular identifier + threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}], + }); + try { + const authClient = new IdentityAuthClient(); + const token = await authClient.getAccessToken(); + if (term !== this.state.filterText) return; // abandon hope + + const lookup = await MatrixClientPeg.get().lookupThreePid( + 'email', + term, + undefined, // callback + token, + ); + if (term !== this.state.filterText) return; // abandon hope + + if (!lookup || !lookup.mxid) { + // We weren't able to find anyone - we're already suggesting the plain email + // as an alternative, so do nothing. + return; + } + + // We append the user suggestion to give the user an option to click + // the email anyways, and so we don't cause things to jump around. In + // theory, the user would see the user pop up and think "ah yes, that + // person!" + const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid); + if (term !== this.state.filterText || !profile) return; // abandon hope + this.setState({ + threepidResultsMixin: [...this.state.threepidResultsMixin, { + user: new DirectoryMember({ + user_id: lookup.mxid, + display_name: profile.displayname, + avatar_url: profile.avatar_url, + }), + userId: lookup.mxid, + }], + }); + } catch (e) { + console.error("Error searching identity server:"); + console.error(e); + this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal + } + } + }, 150); // 150ms debounce (human reaction time + some) + }; + + _showMoreRecents = () => { + this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN}); + }; + + _showMoreSuggestions = () => { + this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN}); + }; + + _toggleMember = (member: Member) => { + const targets = this.state.targets.map(t => t); // cheap clone for mutation + const idx = targets.indexOf(member); + if (idx >= 0) targets.splice(idx, 1); + else targets.push(member); + this.setState({targets}); + }; + + _removeMember = (member: Member) => { + const targets = this.state.targets.map(t => t); // cheap clone for mutation + const idx = targets.indexOf(member); + if (idx >= 0) { + targets.splice(idx, 1); + this.setState({targets}); + } + }; + + _onPaste = async (e) => { + // Prevent the text being pasted into the textarea + e.preventDefault(); + + // Process it as a list of addresses to add instead + const text = e.clipboardData.getData("text"); + const possibleMembers = [ + // If we can avoid hitting the profile endpoint, we should. + ...this.state.recents, + ...this.state.suggestions, + ...this.state.serverResultsMixin, + ...this.state.threepidResultsMixin, + ]; + const toAdd = []; + const failed = []; + const potentialAddresses = text.split(/[\s,]+/); + for (const address of potentialAddresses) { + const member = possibleMembers.find(m => m.userId === address); + if (member) { + toAdd.push(member.user); + continue; + } + + if (address.indexOf('@') > 0 && Email.looksValid(address)) { + toAdd.push(new ThreepidMember(address)); + continue; + } + + if (address[0] !== '@') { + failed.push(address); // not a user ID + continue; + } + + try { + const profile = await MatrixClientPeg.get().getProfileInfo(address); + const displayName = profile ? profile.displayname : null; + const avatarUrl = profile ? profile.avatar_url : null; + toAdd.push(new DirectoryMember({ + user_id: address, + display_name: displayName, + avatar_url: avatarUrl, + })); + } catch (e) { + console.error("Error looking up profile for " + address); + console.error(e); + failed.push(address); + } + } + + if (failed.length > 0) { + const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); + Modal.createTrackedDialog('Invite Paste Fail', '', QuestionDialog, { + title: _t('Failed to find the following users'), + description: _t( + "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s", + {csvNames: failed.join(", ")}, + ), + button: _t('OK'), + }); + } + + this.setState({targets: [...this.state.targets, ...toAdd]}); + }; + + _onClickInputArea = (e) => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + + if (this._editorRef && this._editorRef.current) { + this._editorRef.current.focus(); + } + }; + + _onUseDefaultIdentityServerClick = (e) => { + e.preventDefault(); + + // Update the IS in account data. Actually using it may trigger terms. + // eslint-disable-next-line react-hooks/rules-of-hooks + useDefaultIdentityServer(); + this.setState({canUseIdentityServer: true, tryingIdentityServer: false}); + }; + + _onManageSettingsClick = (e) => { + e.preventDefault(); + dis.dispatch({ action: 'view_user_settings' }); + this._cancel(); + }; + + _renderSection(kind: "recents"|"suggestions") { + let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions; + let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown; + const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this); + const lastActive = (m) => kind === 'recents' ? m.lastActive : null; + const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); + + // Mix in the server results if we have any, but only if we're searching. We track the additional + // members separately because we want to filter sourceMembers but trust the mixin arrays to have + // the right members in them. + let additionalMembers = []; + const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin; + if (this.state.filterText && hasMixins && kind === 'suggestions') { + // We don't want to duplicate members though, so just exclude anyone we've already seen. + const notAlreadyExists = (u: Member): boolean => { + return !sourceMembers.some(m => m.userId === u.userId) + && !additionalMembers.some(m => m.userId === u.userId); + }; + + const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists); + additionalMembers = additionalMembers.concat(...uniqueServerResults); + + const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists); + additionalMembers = additionalMembers.concat(...uniqueThreepidResults); + } + + // Hide the section if there's nothing to filter by + if (sourceMembers.length === 0 && additionalMembers.length === 0) return null; + + // Do some simple filtering on the input before going much further. If we get no results, say so. + if (this.state.filterText) { + const filterBy = this.state.filterText.toLowerCase(); + sourceMembers = sourceMembers + .filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy)); + + if (sourceMembers.length === 0 && additionalMembers.length === 0) { + return ( +
+

{sectionName}

+

{_t("No results")}

+
+ ); + } + } + + // Now we mix in the additional members. Again, we presume these have already been filtered. We + // also assume they are more relevant than our suggestions and prepend them to the list. + sourceMembers = [...additionalMembers, ...sourceMembers]; + + // If we're going to hide one member behind 'show more', just use up the space of the button + // with the member's tile instead. + if (showNum === sourceMembers.length - 1) showNum++; + + // .slice() will return an incomplete array but won't error on us if we go too far + const toRender = sourceMembers.slice(0, showNum); + const hasMore = toRender.length < sourceMembers.length; + + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + let showMore = null; + if (hasMore) { + showMore = ( + + {_t("Show more")} + + ); + } + + const tiles = toRender.map(r => ( + t.userId === r.userId)} + /> + )); + return ( +
+

{sectionName}

+ {tiles} + {showMore} +
+ ); + } + + _renderEditor() { + const targets = this.state.targets.map(t => ( + + )); + const input = ( +